appending .pro to package name

This commit is contained in:
tibbi
2018-11-04 20:19:58 +01:00
parent 1e48edfe39
commit c9fa997fc0
24 changed files with 59 additions and 93 deletions

View File

@ -0,0 +1,11 @@
package com.simplemobiletools.draw.pro
import android.app.Application
import com.simplemobiletools.commons.extensions.checkUseEnglish
class App : Application() {
override fun onCreate() {
super.onCreate()
checkUseEnglish()
}
}

View File

@ -0,0 +1,11 @@
package com.simplemobiletools.draw.pro.actions
import android.graphics.Path
import java.io.Serializable
import java.io.Writer
interface Action : Serializable {
fun perform(path: Path)
fun perform(writer: Writer)
}

View File

@ -0,0 +1,37 @@
package com.simplemobiletools.draw.pro.actions
import android.graphics.Path
import java.io.Writer
import java.security.InvalidParameterException
class Line : Action {
val x: Float
val y: Float
constructor(data: String) {
if (!data.startsWith("L"))
throw InvalidParameterException("The Line data should start with 'L'.")
try {
val xy = data.substring(1).split(",".toRegex()).dropLastWhile(String::isEmpty).toTypedArray()
x = xy[0].trim().toFloat()
y = xy[1].trim().toFloat()
} catch (ignored: Exception) {
throw InvalidParameterException("Error parsing the given Line data.")
}
}
constructor(x: Float, y: Float) {
this.x = x
this.y = y
}
override fun perform(path: Path) {
path.lineTo(x, y)
}
override fun perform(writer: Writer) {
writer.write("L$x,$y")
}
}

View File

@ -0,0 +1,37 @@
package com.simplemobiletools.draw.pro.actions
import android.graphics.Path
import java.io.Writer
import java.security.InvalidParameterException
class Move : Action {
val x: Float
val y: Float
constructor(data: String) {
if (!data.startsWith("M"))
throw InvalidParameterException("The Move data should start with 'M'.")
try {
val xy = data.substring(1).split(",".toRegex()).dropLastWhile(String::isEmpty).toTypedArray()
x = xy[0].trim().toFloat()
y = xy[1].trim().toFloat()
} catch (ignored: Exception) {
throw InvalidParameterException("Error parsing the given Move data.")
}
}
constructor(x: Float, y: Float) {
this.x = x
this.y = y
}
override fun perform(path: Path) {
path.moveTo(x, y)
}
override fun perform(writer: Writer) {
writer.write("M$x,$y")
}
}

View File

@ -0,0 +1,46 @@
package com.simplemobiletools.draw.pro.actions
import android.graphics.Path
import java.io.Writer
import java.security.InvalidParameterException
class Quad : Action {
val x1: Float
val y1: Float
val x2: Float
val y2: Float
constructor(data: String) {
if (!data.startsWith("Q"))
throw InvalidParameterException("The Quad data should start with 'Q'.")
try {
val parts = data.split("\\s+".toRegex()).dropLastWhile(String::isEmpty).toTypedArray()
val xy1 = parts[0].substring(1).split(",".toRegex()).dropLastWhile(String::isEmpty).toTypedArray()
val xy2 = parts[1].split(",".toRegex()).dropLastWhile(String::isEmpty).toTypedArray()
x1 = xy1[0].trim().toFloat()
y1 = xy1[1].trim().toFloat()
x2 = xy2[0].trim().toFloat()
y2 = xy2[1].trim().toFloat()
} catch (ignored: Exception) {
throw InvalidParameterException("Error parsing the given Quad data.")
}
}
constructor(x1: Float, y1: Float, x2: Float, y2: Float) {
this.x1 = x1
this.y1 = y1
this.x2 = x2
this.y2 = y2
}
override fun perform(path: Path) {
path.quadTo(x1, y1, x2, y2)
}
override fun perform(writer: Writer) {
writer.write("Q$x1,$y1 $x2,$y2")
}
}

View File

@ -0,0 +1,467 @@
package com.simplemobiletools.draw.pro.activities
import android.app.Activity
import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.Bitmap
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.view.Menu
import android.view.MenuItem
import android.view.WindowManager
import android.webkit.MimeTypeMap
import android.widget.SeekBar
import com.simplemobiletools.commons.dialogs.ColorPickerDialog
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.LICENSE_GLIDE
import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE
import com.simplemobiletools.commons.models.FAQItem
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.commons.models.Release
import com.simplemobiletools.draw.pro.BuildConfig
import com.simplemobiletools.draw.pro.R
import com.simplemobiletools.draw.pro.dialogs.SaveImageDialog
import com.simplemobiletools.draw.pro.extensions.config
import com.simplemobiletools.draw.pro.helpers.JPG
import com.simplemobiletools.draw.pro.helpers.PNG
import com.simplemobiletools.draw.pro.helpers.SVG
import com.simplemobiletools.draw.pro.interfaces.CanvasListener
import com.simplemobiletools.draw.pro.models.Svg
import kotlinx.android.synthetic.main.activity_main.*
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.OutputStream
class MainActivity : SimpleActivity(), CanvasListener {
private val FOLDER_NAME = "images"
private val FILE_NAME = "simple-draw.png"
private val BITMAP_PATH = "bitmap_path"
private var defaultPath = ""
private var defaultFilename = ""
private var defaultExtension = PNG
private var intentUri: Uri? = null
private var color = 0
private var brushSize = 0f
private var isEraserOn = false
private var isImageCaptureIntent = false
private var lastBitmapPath = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
appLaunched(BuildConfig.APPLICATION_ID)
my_canvas.mListener = this
stroke_width_bar.setOnSeekBarChangeListener(onStrokeWidthBarChangeListener)
setBackgroundColor(config.canvasBackgroundColor)
setColor(config.brushColor)
defaultPath = config.lastSaveFolder
brushSize = config.brushSize
updateBrushSize()
stroke_width_bar.progress = brushSize.toInt()
color_picker.setOnClickListener { pickColor() }
undo.setOnClickListener { my_canvas.undo() }
eraser.setOnClickListener { eraserClicked() }
redo.setOnClickListener { my_canvas.redo() }
checkIntents()
if (!isImageCaptureIntent) {
checkWhatsNewDialog()
}
}
override fun onResume() {
super.onResume()
val isShowBrushSizeEnabled = config.showBrushSize
stroke_width_bar.beVisibleIf(isShowBrushSizeEnabled)
stroke_width_preview.beVisibleIf(isShowBrushSizeEnabled)
my_canvas.setAllowZooming(config.allowZoomingCanvas)
updateTextColors(main_holder)
if (config.preventPhoneFromSleeping) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
requestedOrientation = if (config.forcePortraitMode) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
override fun onPause() {
super.onPause()
config.brushColor = color
config.brushSize = brushSize
if (config.preventPhoneFromSleeping) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
override fun onDestroy() {
super.onDestroy()
my_canvas.mListener = null
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu, menu)
menu.apply {
findItem(R.id.menu_confirm).isVisible = isImageCaptureIntent
findItem(R.id.menu_save).isVisible = !isImageCaptureIntent
findItem(R.id.menu_share).isVisible = !isImageCaptureIntent
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_confirm -> confirmImage()
R.id.menu_save -> trySaveImage()
R.id.menu_share -> shareImage()
R.id.clear -> clearCanvas()
R.id.open_file -> tryOpenFile()
R.id.change_background -> changeBackgroundClicked()
R.id.settings -> launchSettings()
R.id.about -> launchAbout()
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun launchSettings() {
startActivity(Intent(applicationContext, SettingsActivity::class.java))
}
private fun launchAbout() {
val licenses = LICENSE_GLIDE
val faqItems = arrayListOf(
FAQItem(R.string.faq_2_title_commons, R.string.faq_2_text_commons)
)
startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, false)
}
private fun tryOpenFile() {
getStoragePermission {
openFile()
}
}
private fun openFile() {
val path = if (isImageCaptureIntent) "" else defaultPath
FilePickerDialog(this, path) {
openPath(it)
}
}
private fun checkIntents() {
if (intent?.action == Intent.ACTION_SEND && intent.type.startsWith("image/")) {
getStoragePermission {
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
tryOpenUri(uri)
}
}
if (intent?.action == Intent.ACTION_SEND_MULTIPLE && intent.type.startsWith("image/")) {
getStoragePermission {
val imageUris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
imageUris.any { tryOpenUri(it) }
}
}
if (intent?.action == Intent.ACTION_VIEW && intent.data != null) {
getStoragePermission {
val path = getRealPathFromURI(intent.data) ?: intent.dataString
openPath(path)
}
}
if (intent?.action == MediaStore.ACTION_IMAGE_CAPTURE) {
val output = intent.extras?.get(MediaStore.EXTRA_OUTPUT)
if (output != null && output is Uri) {
isImageCaptureIntent = true
intentUri = output
defaultPath = output.path
invalidateOptionsMenu()
}
}
}
private fun getStoragePermission(callback: () -> Unit) {
handlePermission(PERMISSION_WRITE_STORAGE) {
if (it) {
callback()
} else {
toast(R.string.no_storage_permissions)
}
}
}
private fun tryOpenUri(uri: Uri) = when {
uri.scheme == "file" -> openPath(uri.path)
uri.scheme == "content" -> openUri(uri, intent)
else -> false
}
private fun openPath(path: String) = when {
path.endsWith(".svg") -> {
my_canvas.mBackgroundBitmap = null
Svg.loadSvg(this, File(path), my_canvas)
defaultExtension = SVG
true
}
File(path).isImageSlow() -> {
lastBitmapPath = path
my_canvas.drawBitmap(this, path)
defaultExtension = JPG
true
}
else -> {
toast(R.string.invalid_file_format)
false
}
}
private fun openUri(uri: Uri, intent: Intent): Boolean {
val mime = MimeTypeMap.getSingleton()
val type = mime.getExtensionFromMimeType(contentResolver.getType(uri)) ?: intent.type
return when (type) {
"svg", "image/svg+xml" -> {
my_canvas.mBackgroundBitmap = null
Svg.loadSvg(this, uri, my_canvas)
defaultExtension = SVG
true
}
"jpg", "jpeg", "png" -> {
my_canvas.drawBitmap(this, uri)
defaultExtension = JPG
true
}
else -> {
toast(R.string.invalid_file_format)
false
}
}
}
private fun eraserClicked() {
isEraserOn = !isEraserOn
updateEraserState()
}
private fun updateEraserState() {
eraser.setImageDrawable(resources.getDrawable(if (isEraserOn) R.drawable.ic_eraser_on else R.drawable.ic_eraser_off))
my_canvas.toggleEraser(isEraserOn)
}
private fun changeBackgroundClicked() {
val oldColor = (my_canvas.background as ColorDrawable).color
ColorPickerDialog(this, oldColor) { wasPositivePressed, color ->
if (wasPositivePressed) {
config.canvasBackgroundColor = color
setBackgroundColor(color)
}
}
}
private fun confirmImage() {
if (intentUri?.scheme == "content") {
val outputStream = contentResolver.openOutputStream(intentUri)
saveToOutputStream(outputStream, defaultPath.getCompressionFormat())
} else {
handlePermission(PERMISSION_WRITE_STORAGE) {
val fileDirItem = FileDirItem(defaultPath, defaultPath.getFilenameFromPath())
getFileOutputStream(fileDirItem, true) {
saveToOutputStream(it, defaultPath.getCompressionFormat())
}
}
}
}
private fun saveToOutputStream(outputStream: OutputStream?, format: Bitmap.CompressFormat) {
if (outputStream == null) {
toast(R.string.unknown_error_occurred)
return
}
outputStream.use {
my_canvas.getBitmap().compress(format, 70, it)
}
setResult(Activity.RESULT_OK)
finish()
}
private fun trySaveImage() {
getStoragePermission {
saveImage()
}
}
private fun saveImage() {
SaveImageDialog(this, defaultExtension, defaultPath, defaultFilename) {
saveFile(it)
defaultPath = it.getParentPath()
defaultFilename = it.getFilenameFromPath()
defaultFilename = defaultFilename.substring(0, defaultFilename.lastIndexOf("."))
defaultExtension = it.getFilenameExtension()
config.lastSaveFolder = defaultPath
}
}
private fun saveFile(path: String) {
when (path.getFilenameExtension()) {
SVG -> Svg.saveSvg(this, path, my_canvas)
else -> saveImageFile(path)
}
rescanPaths(arrayListOf(path)) {}
}
private fun saveImageFile(path: String) {
val fileDirItem = FileDirItem(path, path.getFilenameFromPath())
getFileOutputStream(fileDirItem, true) {
if (it != null) {
writeToOutputStream(path, it)
toast(R.string.file_saved)
} else {
toast(R.string.unknown_error_occurred)
}
}
}
private fun writeToOutputStream(path: String, out: OutputStream) {
out.use {
my_canvas.getBitmap().compress(path.getCompressionFormat(), 70, out)
}
}
private fun shareImage() {
getImagePath(my_canvas.getBitmap()) {
if (it != null) {
sharePathIntent(it, BuildConfig.APPLICATION_ID)
} else {
toast(R.string.unknown_error_occurred)
}
}
}
private fun getImagePath(bitmap: Bitmap, callback: (path: String?) -> Unit) {
val bytes = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 0, bytes)
val folder = File(cacheDir, FOLDER_NAME)
if (!folder.exists()) {
if (!folder.mkdir()) {
callback(null)
return
}
}
val newPath = "$folder/$FILE_NAME"
val fileDirItem = FileDirItem(newPath, FILE_NAME)
getFileOutputStream(fileDirItem, true) {
if (it != null) {
try {
it.write(bytes.toByteArray())
callback(newPath)
} catch (e: Exception) {
} finally {
it.close()
}
} else {
callback("")
}
}
}
private fun clearCanvas() {
my_canvas.clearCanvas()
defaultExtension = PNG
defaultPath = ""
lastBitmapPath = ""
}
private fun pickColor() {
ColorPickerDialog(this, color) { wasPositivePressed, color ->
if (wasPositivePressed) {
setColor(color)
}
}
}
fun setBackgroundColor(pickedColor: Int) {
val contrastColor = pickedColor.getContrastColor()
undo.applyColorFilter(contrastColor)
eraser.applyColorFilter(contrastColor)
redo.applyColorFilter(contrastColor)
my_canvas.updateBackgroundColor(pickedColor)
defaultExtension = PNG
getBrushPreviewView().setStroke(getBrushStrokeSize(), contrastColor)
}
private fun setColor(pickedColor: Int) {
color = pickedColor
color_picker.setFillWithStroke(color, config.canvasBackgroundColor.getContrastColor())
my_canvas.setColor(color)
isEraserOn = false
updateEraserState()
getBrushPreviewView().setColor(color)
}
private fun getBrushPreviewView() = stroke_width_preview.background as GradientDrawable
private fun getBrushStrokeSize() = resources.getDimension(R.dimen.preview_dot_stroke_size).toInt()
override fun toggleUndoVisibility(visible: Boolean) {
undo.beVisibleIf(visible)
}
override fun toggleRedoVisibility(visible: Boolean) {
redo.beVisibleIf(visible)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(BITMAP_PATH, lastBitmapPath)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
lastBitmapPath = savedInstanceState.getString(BITMAP_PATH)
if (lastBitmapPath.isNotEmpty()) {
openPath(lastBitmapPath)
}
}
private var onStrokeWidthBarChangeListener: SeekBar.OnSeekBarChangeListener = object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
brushSize = progress.toFloat()
updateBrushSize()
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
}
private fun updateBrushSize() {
my_canvas.setBrushSize(brushSize)
val scale = Math.max(0.03f, brushSize / 100f)
stroke_width_preview.scaleX = scale
stroke_width_preview.scaleY = scale
}
private fun checkWhatsNewDialog() {
arrayListOf<Release>().apply {
add(Release(18, R.string.release_18))
add(Release(20, R.string.release_20))
add(Release(38, R.string.release_38))
checkWhatsNew(this, BuildConfig.VERSION_CODE)
}
}
}

View File

@ -0,0 +1,76 @@
package com.simplemobiletools.draw.pro.activities
import android.os.Bundle
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.extensions.updateTextColors
import com.simplemobiletools.draw.pro.R
import com.simplemobiletools.draw.pro.extensions.config
import kotlinx.android.synthetic.main.activity_settings.*
import java.util.*
class SettingsActivity : SimpleActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
}
override fun onResume() {
super.onResume()
setupCustomizeColors()
setupUseEnglish()
setupPreventPhoneFromSleeping()
setupBrushSize()
setupAllowZoomingCanvas()
setupForcePortraitMode()
updateTextColors(settings_holder)
}
private fun setupCustomizeColors() {
settings_customize_colors_holder.setOnClickListener {
startCustomizationActivity()
}
}
private fun setupUseEnglish() {
settings_use_english_holder.beVisibleIf(config.wasUseEnglishToggled || Locale.getDefault().language != "en")
settings_use_english.isChecked = config.useEnglish
settings_use_english_holder.setOnClickListener {
settings_use_english.toggle()
config.useEnglish = settings_use_english.isChecked
System.exit(0)
}
}
private fun setupPreventPhoneFromSleeping() {
settings_prevent_phone_from_sleeping.isChecked = config.preventPhoneFromSleeping
settings_prevent_phone_from_sleeping_holder.setOnClickListener {
settings_prevent_phone_from_sleeping.toggle()
config.preventPhoneFromSleeping = settings_prevent_phone_from_sleeping.isChecked
}
}
private fun setupBrushSize() {
settings_show_brush_size.isChecked = config.showBrushSize
settings_show_brush_size_holder.setOnClickListener {
settings_show_brush_size.toggle()
config.showBrushSize = settings_show_brush_size.isChecked
}
}
private fun setupAllowZoomingCanvas() {
settings_allow_zooming_canvas.isChecked = config.allowZoomingCanvas
settings_allow_zooming_canvas_holder.setOnClickListener {
settings_allow_zooming_canvas.toggle()
config.allowZoomingCanvas = settings_allow_zooming_canvas.isChecked
}
}
private fun setupForcePortraitMode() {
settings_force_portrait.isChecked = config.forcePortraitMode
settings_force_portrait_holder.setOnClickListener {
settings_force_portrait.toggle()
config.forcePortraitMode = settings_force_portrait.isChecked
}
}
}

View File

@ -0,0 +1,30 @@
package com.simplemobiletools.draw.pro.activities
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.draw.pro.R
open class SimpleActivity : BaseSimpleActivity() {
override fun getAppIconIDs() = arrayListOf(
R.mipmap.ic_launcher_red,
R.mipmap.ic_launcher_pink,
R.mipmap.ic_launcher_purple,
R.mipmap.ic_launcher_deep_purple,
R.mipmap.ic_launcher_indigo,
R.mipmap.ic_launcher_blue,
R.mipmap.ic_launcher_light_blue,
R.mipmap.ic_launcher_cyan,
R.mipmap.ic_launcher_teal,
R.mipmap.ic_launcher_green,
R.mipmap.ic_launcher_light_green,
R.mipmap.ic_launcher_lime,
R.mipmap.ic_launcher_yellow,
R.mipmap.ic_launcher_amber,
R.mipmap.ic_launcher,
R.mipmap.ic_launcher_deep_orange,
R.mipmap.ic_launcher_brown,
R.mipmap.ic_launcher_blue_grey,
R.mipmap.ic_launcher_grey_black
)
override fun getAppLauncherName() = getString(R.string.app_launcher_name)
}

View File

@ -0,0 +1,11 @@
package com.simplemobiletools.draw.pro.activities
import android.content.Intent
import com.simplemobiletools.commons.activities.BaseSplashActivity
class SplashActivity : BaseSplashActivity() {
override fun initActivity() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}

View File

@ -0,0 +1,82 @@
package com.simplemobiletools.draw.pro.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.draw.pro.R
import com.simplemobiletools.draw.pro.activities.SimpleActivity
import com.simplemobiletools.draw.pro.helpers.JPG
import com.simplemobiletools.draw.pro.helpers.PNG
import com.simplemobiletools.draw.pro.helpers.SVG
import kotlinx.android.synthetic.main.dialog_save_image.view.*
class SaveImageDialog(val activity: SimpleActivity, val defaultExtension: String, val defaultPath: String, val defaultFilename: String,
callback: (savePath: String) -> Unit) {
private val SIMPLE_DRAW = "Simple Draw"
init {
val initialFilename = getInitialFilename()
var folder = if (defaultPath.isEmpty()) "${activity.internalStoragePath}/$SIMPLE_DRAW" else defaultPath
val view = activity.layoutInflater.inflate(R.layout.dialog_save_image, null).apply {
save_image_filename.setText(initialFilename)
save_image_radio_group.check(when (defaultExtension) {
JPG -> R.id.save_image_radio_jpg
SVG -> R.id.save_image_radio_svg
else -> R.id.save_image_radio_png
})
save_image_path.text = activity.humanizePath(folder)
save_image_path.setOnClickListener {
FilePickerDialog(activity, folder, false, showFAB = true) {
save_image_path.text = activity.humanizePath(it)
folder = it
}
}
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this, R.string.save_as) {
showKeyboard(view.save_image_filename)
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val filename = view.save_image_filename.value
if (filename.isEmpty()) {
activity.toast(R.string.filename_cannot_be_empty)
return@setOnClickListener
}
val extension = when (view.save_image_radio_group.checkedRadioButtonId) {
R.id.save_image_radio_png -> PNG
R.id.save_image_radio_svg -> SVG
else -> JPG
}
val newPath = "${folder.trimEnd('/')}/$filename.$extension"
if (!newPath.getFilenameFromPath().isAValidFilename()) {
activity.toast(R.string.filename_invalid_characters)
return@setOnClickListener
}
if (activity.getDoesFilePathExist(newPath)) {
val title = String.format(activity.getString(R.string.file_already_exists_overwrite), newPath.getFilenameFromPath())
ConfirmationDialog(activity, title) {
callback(newPath)
dismiss()
}
} else {
callback(newPath)
dismiss()
}
}
}
}
}
private fun getInitialFilename(): String {
val newFilename = "image_${activity.getCurrentFormattedDateTime()}"
return if (defaultFilename.isEmpty()) newFilename else defaultFilename
}
}

View File

@ -0,0 +1,6 @@
package com.simplemobiletools.draw.pro.extensions
import android.content.Context
import com.simplemobiletools.draw.pro.helpers.Config
val Context.config: Config get() = Config.newInstance(applicationContext)

View File

@ -0,0 +1,40 @@
package com.simplemobiletools.draw.pro.helpers
import android.content.Context
import android.graphics.Color
import com.simplemobiletools.commons.helpers.BaseConfig
import com.simplemobiletools.draw.pro.R
class Config(context: Context) : BaseConfig(context) {
companion object {
fun newInstance(context: Context) = Config(context)
}
var showBrushSize: Boolean
get() = prefs.getBoolean(SHOW_BRUSH_SIZE, true)
set(showBrushSize) = prefs.edit().putBoolean(SHOW_BRUSH_SIZE, showBrushSize).apply()
var brushColor: Int
get() = prefs.getInt(BRUSH_COLOR, context.resources.getColor(R.color.color_primary))
set(color) = prefs.edit().putInt(BRUSH_COLOR, color).apply()
var brushSize: Float
get() = prefs.getFloat(BRUSH_SIZE, 50f)
set(brushSize) = prefs.edit().putFloat(BRUSH_SIZE, brushSize).apply()
var canvasBackgroundColor: Int
get() = prefs.getInt(CANVAS_BACKGROUND_COLOR, Color.WHITE)
set(canvasBackgroundColor) = prefs.edit().putInt(CANVAS_BACKGROUND_COLOR, canvasBackgroundColor).apply()
var lastSaveFolder: String
get() = prefs.getString(LAST_SAVE_FOLDER, "")
set(lastSaveFolder) = prefs.edit().putString(LAST_SAVE_FOLDER, lastSaveFolder).apply()
var allowZoomingCanvas: Boolean
get() = prefs.getBoolean(ALLOW_ZOOMING_CANVAS, false)
set(allowZoomingCanvas) = prefs.edit().putBoolean(ALLOW_ZOOMING_CANVAS, allowZoomingCanvas).apply()
var forcePortraitMode: Boolean
get() = prefs.getBoolean(FORCE_PORTRAIT_MODE, false)
set(forcePortraitMode) = prefs.edit().putBoolean(FORCE_PORTRAIT_MODE, forcePortraitMode).apply()
}

View File

@ -0,0 +1,13 @@
package com.simplemobiletools.draw.pro.helpers
const val BRUSH_COLOR = "brush_color"
const val CANVAS_BACKGROUND_COLOR = "canvas_background_color"
const val SHOW_BRUSH_SIZE = "show_brush_size"
const val BRUSH_SIZE = "brush_size_2"
const val LAST_SAVE_FOLDER = "last_save_folder"
const val ALLOW_ZOOMING_CANVAS = "allow_zooming_canvas"
const val FORCE_PORTRAIT_MODE = "force_portrait_mode"
const val PNG = "png"
const val SVG = "svg"
const val JPG = "jpg"

View File

@ -0,0 +1,7 @@
package com.simplemobiletools.draw.pro.interfaces
interface CanvasListener {
fun toggleUndoVisibility(visible: Boolean)
fun toggleRedoVisibility(visible: Boolean)
}

View File

@ -0,0 +1,41 @@
package com.simplemobiletools.draw.pro.models
import android.os.Parcel
import android.os.Parcelable
import android.view.View
import java.util.*
internal class MyParcelable : View.BaseSavedState {
var paths = LinkedHashMap<MyPath, PaintOptions>()
constructor(superState: Parcelable) : super(superState)
constructor(parcel: Parcel) : super(parcel) {
val size = parcel.readInt()
for (i in 0 until size) {
val key = parcel.readSerializable() as MyPath
val paintOptions = PaintOptions(parcel.readInt(), parcel.readFloat(), parcel.readInt() == 1)
paths[key] = paintOptions
}
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(paths.size)
for ((path, paintOptions) in paths) {
out.writeSerializable(path)
out.writeInt(paintOptions.color)
out.writeFloat(paintOptions.strokeWidth)
out.writeInt(if (paintOptions.isEraser) 1 else 0)
}
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<MyParcelable> = object : Parcelable.Creator<MyParcelable> {
override fun createFromParcel(source: Parcel) = MyParcelable(source)
override fun newArray(size: Int) = arrayOf<MyParcelable>()
}
}
}

View File

@ -0,0 +1,82 @@
package com.simplemobiletools.draw.pro.models
import android.app.Activity
import android.graphics.Path
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.draw.pro.R
import com.simplemobiletools.draw.pro.actions.Action
import com.simplemobiletools.draw.pro.actions.Line
import com.simplemobiletools.draw.pro.actions.Move
import com.simplemobiletools.draw.pro.actions.Quad
import java.io.ObjectInputStream
import java.io.Serializable
import java.security.InvalidParameterException
import java.util.*
// https://stackoverflow.com/a/8127953
class MyPath : Path(), Serializable {
val actions = LinkedList<Action>()
private fun readObject(inputStream: ObjectInputStream) {
inputStream.defaultReadObject()
val copiedActions = actions.map { it }
copiedActions.forEach {
it.perform(this)
}
}
fun readObject(pathData: String, activity: Activity) {
val tokens = pathData.split("\\s+".toRegex()).dropLastWhile(String::isEmpty).toTypedArray()
var i = 0
try {
while (i < tokens.size) {
when (tokens[i][0]) {
'M' -> addAction(Move(tokens[i]))
'L' -> addAction(Line(tokens[i]))
'Q' -> {
// Quad actions are of the following form:
// "Qx1,y1 x2,y2"
// Since we split the tokens by whitespace, we need to join them again
if (i + 1 >= tokens.size)
throw InvalidParameterException("Error parsing the data for a Quad.")
addAction(Quad(tokens[i] + " " + tokens[i + 1]))
++i
}
}
++i
}
} catch (e: Exception) {
activity.toast(R.string.unknown_error_occurred)
}
}
override fun reset() {
actions.clear()
super.reset()
}
private fun addAction(action: Action) {
when (action) {
is Move -> moveTo(action.x, action.y)
is Line -> lineTo(action.x, action.y)
is Quad -> quadTo(action.x1, action.y1, action.x2, action.y2)
}
}
override fun moveTo(x: Float, y: Float) {
actions.add(Move(x, y))
super.moveTo(x, y)
}
override fun lineTo(x: Float, y: Float) {
actions.add(Line(x, y))
super.lineTo(x, y)
}
override fun quadTo(x1: Float, y1: Float, x2: Float, y2: Float) {
actions.add(Quad(x1, y1, x2, y2))
super.quadTo(x1, y1, x2, y2)
}
}

View File

@ -0,0 +1,7 @@
package com.simplemobiletools.draw.pro.models
import android.graphics.Color
data class PaintOptions(var color: Int = Color.BLACK, var strokeWidth: Float = 5f, var isEraser: Boolean = false) {
fun getColorToExport() = if (isEraser) "none" else "#${Integer.toHexString(color).substring(2)}"
}

View File

@ -0,0 +1,141 @@
package com.simplemobiletools.draw.pro.models
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.sax.RootElement
import android.util.Xml
import com.simplemobiletools.commons.extensions.getFileOutputStream
import com.simplemobiletools.commons.extensions.getFilenameFromPath
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.draw.pro.R
import com.simplemobiletools.draw.pro.activities.MainActivity
import com.simplemobiletools.draw.pro.activities.SimpleActivity
import com.simplemobiletools.draw.pro.views.MyCanvas
import java.io.*
import java.util.*
object Svg {
fun saveSvg(activity: SimpleActivity, path: String, canvas: MyCanvas) {
val backgroundColor = (canvas.background as ColorDrawable).color
activity.getFileOutputStream(FileDirItem(path, path.getFilenameFromPath()), true) {
if (it != null) {
val writer = BufferedWriter(OutputStreamWriter(it))
writeSvg(writer, backgroundColor, canvas.mPaths, canvas.width, canvas.height)
writer.close()
activity.toast(R.string.file_saved)
} else {
activity.toast(R.string.unknown_error_occurred)
}
}
}
private fun writeSvg(writer: Writer, backgroundColor: Int, paths: Map<MyPath, PaintOptions>, width: Int, height: Int) {
writer.apply {
write("<svg width=\"$width\" height=\"$height\" xmlns=\"http://www.w3.org/2000/svg\">")
write("<rect width=\"$width\" height=\"$height\" fill=\"#${Integer.toHexString(backgroundColor).substring(2)}\"/>")
for ((key, value) in paths) {
writePath(this, key, value)
}
write("</svg>")
}
}
private fun writePath(writer: Writer, path: MyPath, options: PaintOptions) {
writer.apply {
write("<path d=\"")
path.actions.forEach {
it.perform(this)
write(" ")
}
write("\" fill=\"none\" stroke=\"")
write(options.getColorToExport())
write("\" stroke-width=\"")
write(options.strokeWidth.toString())
write("\" stroke-linecap=\"round\"/>")
}
}
fun loadSvg(activity: MainActivity, fileOrUri: Any, canvas: MyCanvas) {
val svg = parseSvg(activity, fileOrUri)
canvas.clearCanvas()
activity.setBackgroundColor(svg.background!!.color)
svg.paths.forEach {
val path = MyPath()
path.readObject(it.data, activity)
val options = PaintOptions(it.color, it.strokeWidth, it.isEraser)
canvas.addPath(path, options)
}
}
private fun parseSvg(activity: MainActivity, fileOrUri: Any): SSvg {
var inputStream: InputStream? = null
val svg = SSvg()
try {
inputStream = when (fileOrUri) {
is File -> FileInputStream(fileOrUri)
is Uri -> activity.contentResolver.openInputStream(fileOrUri)
else -> null
}
// Actual parsing (http://stackoverflow.com/a/4828765)
val ns = "http://www.w3.org/2000/svg"
val root = RootElement(ns, "svg")
val rectElement = root.getChild(ns, "rect")
val pathElement = root.getChild(ns, "path")
root.setStartElementListener { attributes ->
val width = attributes.getValue("width").toInt()
val height = attributes.getValue("height").toInt()
svg.setSize(width, height)
}
rectElement.setStartElementListener { attributes ->
val width = attributes.getValue("width").toInt()
val height = attributes.getValue("height").toInt()
val color = Color.parseColor(attributes.getValue("fill"))
if (svg.background != null)
throw UnsupportedOperationException("Unsupported SVG, should only have one <rect>.")
svg.background = SRect(width, height, color)
}
pathElement.setStartElementListener { attributes ->
val d = attributes.getValue("d")
val width = attributes.getValue("stroke-width").toFloat()
val stroke = attributes.getValue("stroke")
val isEraser = stroke == "none"
val color = if (isEraser) 0 else Color.parseColor(stroke)
svg.paths.add(SPath(d, color, width, isEraser))
}
Xml.parse(inputStream, Xml.Encoding.UTF_8, root.contentHandler)
} finally {
inputStream?.close()
}
return svg
}
private class SSvg : Serializable {
var background: SRect? = null
val paths: ArrayList<SPath> = ArrayList()
private var width = 0
private var height = 0
internal fun setSize(w: Int, h: Int) {
width = w
height = h
}
}
private class SRect(val width: Int, val height: Int, val color: Int) : Serializable
private class SPath(var data: String, var color: Int, var strokeWidth: Float, var isEraser: Boolean) : Serializable
}

View File

@ -0,0 +1,323 @@
package com.simplemobiletools.draw.pro.views
import android.app.Activity
import android.content.Context
import android.graphics.*
import android.os.Parcelable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.draw.pro.R
import com.simplemobiletools.draw.pro.interfaces.CanvasListener
import com.simplemobiletools.draw.pro.models.MyParcelable
import com.simplemobiletools.draw.pro.models.MyPath
import com.simplemobiletools.draw.pro.models.PaintOptions
import java.util.*
import java.util.concurrent.ExecutionException
class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val MIN_ERASER_WIDTH = 20f
var mPaths = LinkedHashMap<MyPath, PaintOptions>()
var mBackgroundBitmap: Bitmap? = null
var mListener: CanvasListener? = null
private var mLastPaths = LinkedHashMap<MyPath, PaintOptions>()
private var mLastBackgroundBitmap: Bitmap? = null
private var mUndonePaths = LinkedHashMap<MyPath, PaintOptions>()
private var mPaint = Paint()
private var mPath = MyPath()
private var mPaintOptions = PaintOptions()
private var mCurX = 0f
private var mCurY = 0f
private var mStartX = 0f
private var mStartY = 0f
private var mCurrBrushSize = 0f
private var mIsSaving = false
private var mAllowZooming = true
private var mIsEraserOn = false
private var mWasMultitouch = false
private var mBackgroundColor = 0
private var mCenter: PointF? = null
private var mScaleDetector: ScaleGestureDetector? = null
private var mScaleFactor = 1f
init {
mPaint.apply {
color = mPaintOptions.color
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
strokeWidth = mPaintOptions.strokeWidth
isAntiAlias = true
}
mScaleDetector = ScaleGestureDetector(context, ScaleListener())
pathsUpdated()
}
fun undo() {
if (mPaths.isEmpty() && mLastPaths.isNotEmpty()) {
mPaths = mLastPaths.clone() as LinkedHashMap<MyPath, PaintOptions>
mBackgroundBitmap = mLastBackgroundBitmap
mLastPaths.clear()
pathsUpdated()
invalidate()
return
}
if (mPaths.isEmpty()) {
return
}
val lastPath = mPaths.values.lastOrNull()
val lastKey = mPaths.keys.lastOrNull()
mPaths.remove(lastKey)
if (lastPath != null && lastKey != null) {
mUndonePaths[lastKey] = lastPath
mListener?.toggleRedoVisibility(true)
}
pathsUpdated()
invalidate()
}
fun redo() {
if (mUndonePaths.keys.isEmpty()) {
mListener?.toggleRedoVisibility(false)
return
}
val lastKey = mUndonePaths.keys.last()
addPath(lastKey, mUndonePaths.values.last())
mUndonePaths.remove(lastKey)
if (mUndonePaths.isEmpty()) {
mListener?.toggleRedoVisibility(false)
}
invalidate()
}
fun toggleEraser(isEraserOn: Boolean) {
mIsEraserOn = isEraserOn
mPaintOptions.isEraser = isEraserOn
invalidate()
}
fun setColor(newColor: Int) {
mPaintOptions.color = newColor
}
fun updateBackgroundColor(newColor: Int) {
mBackgroundColor = newColor
setBackgroundColor(newColor)
mBackgroundBitmap = null
}
fun setBrushSize(newBrushSize: Float) {
mCurrBrushSize = newBrushSize
mPaintOptions.strokeWidth = resources.getDimension(R.dimen.full_brush_size) * (newBrushSize / mScaleFactor / 100f)
}
fun setAllowZooming(allowZooming: Boolean) {
mAllowZooming = allowZooming
}
fun getBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.WHITE)
mIsSaving = true
draw(canvas)
mIsSaving = false
return bitmap
}
fun drawBitmap(activity: Activity, path: Any) {
Thread {
val size = Point()
activity.windowManager.defaultDisplay.getSize(size)
val options = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.disallowHardwareConfig()
.fitCenter()
try {
val builder = Glide.with(context)
.asBitmap()
.load(path)
.apply(options)
.into(size.x, size.y)
mBackgroundBitmap = builder.get()
activity.runOnUiThread {
invalidate()
}
} catch (e: ExecutionException) {
val errorMsg = String.format(activity.getString(R.string.failed_to_load_image), path)
activity.toast(errorMsg)
}
}.start()
}
fun addPath(path: MyPath, options: PaintOptions) {
mPaths[path] = options
pathsUpdated()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.save()
if (mCenter == null) {
mCenter = PointF(width / 2f, height / 2f)
}
canvas.scale(mScaleFactor, mScaleFactor, mCenter!!.x, mCenter!!.y)
if (mBackgroundBitmap != null) {
val left = (width - mBackgroundBitmap!!.width) / 2
val top = (height - mBackgroundBitmap!!.height) / 2
canvas.drawBitmap(mBackgroundBitmap, left.toFloat(), top.toFloat(), null)
}
for ((key, value) in mPaths) {
changePaint(value)
canvas.drawPath(key, mPaint)
}
changePaint(mPaintOptions)
canvas.drawPath(mPath, mPaint)
canvas.restore()
}
private fun changePaint(paintOptions: PaintOptions) {
mPaint.color = if (paintOptions.isEraser) mBackgroundColor else paintOptions.color
mPaint.strokeWidth = paintOptions.strokeWidth
if (paintOptions.isEraser && mPaint.strokeWidth < MIN_ERASER_WIDTH) {
mPaint.strokeWidth = MIN_ERASER_WIDTH
}
}
fun clearCanvas() {
mLastPaths = mPaths.clone() as LinkedHashMap<MyPath, PaintOptions>
mLastBackgroundBitmap = mBackgroundBitmap
mBackgroundBitmap = null
mPath.reset()
mPaths.clear()
pathsUpdated()
invalidate()
}
private fun actionDown(x: Float, y: Float) {
mPath.reset()
mPath.moveTo(x, y)
mCurX = x
mCurY = y
}
private fun actionMove(x: Float, y: Float) {
mPath.quadTo(mCurX, mCurY, (x + mCurX) / 2, (y + mCurY) / 2)
mCurX = x
mCurY = y
}
private fun actionUp() {
if (!mWasMultitouch) {
mPath.lineTo(mCurX, mCurY)
// draw a dot on click
if (mStartX == mCurX && mStartY == mCurY) {
mPath.lineTo(mCurX, mCurY + 2)
mPath.lineTo(mCurX + 1, mCurY + 2)
mPath.lineTo(mCurX + 1, mCurY)
}
}
mPaths[mPath] = mPaintOptions
pathsUpdated()
mPath = MyPath()
mPaintOptions = PaintOptions(mPaintOptions.color, mPaintOptions.strokeWidth, mPaintOptions.isEraser)
}
private fun pathsUpdated() {
mListener?.toggleUndoVisibility(mPaths.isNotEmpty() || mLastPaths.isNotEmpty())
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (mAllowZooming) {
mScaleDetector!!.onTouchEvent(event)
}
var x = event.x
var y = event.y
if (mScaleFactor != 1f) {
val fullWidth = width / mScaleFactor
var curTouchX = fullWidth * x / width
curTouchX -= (fullWidth / 2) * (1 - mScaleFactor)
x = curTouchX
val fullHeight = height / mScaleFactor
var curTouchY = fullHeight * y / height
curTouchY -= (fullHeight / 2) * (1 - mScaleFactor)
y = curTouchY
}
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
mWasMultitouch = false
mStartX = x
mStartY = y
actionDown(x, y)
mUndonePaths.clear()
mListener?.toggleRedoVisibility(false)
}
MotionEvent.ACTION_MOVE -> {
if (!mAllowZooming || (!mScaleDetector!!.isInProgress && event.pointerCount == 1 && !mWasMultitouch)) {
actionMove(x, y)
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> actionUp()
MotionEvent.ACTION_POINTER_DOWN -> mWasMultitouch = true
}
invalidate()
return true
}
public override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
val savedState = MyParcelable(superState)
savedState.paths = mPaths
return savedState
}
public override fun onRestoreInstanceState(state: Parcelable) {
if (state !is MyParcelable) {
super.onRestoreInstanceState(state)
return
}
super.onRestoreInstanceState(state.superState)
mPaths = state.paths
pathsUpdated()
}
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
mScaleFactor *= detector.scaleFactor
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f))
setBrushSize(mCurrBrushSize)
invalidate()
return true
}
}
}