mirror of
https://github.com/SimpleMobileTools/Simple-Draw.git
synced 2025-06-05 21:59:17 +02:00
appending .pro to package name
This commit is contained in:
11
app/src/main/kotlin/com/simplemobiletools/draw/pro/App.kt
Normal file
11
app/src/main/kotlin/com/simplemobiletools/draw/pro/App.kt
Normal 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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
@ -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()
|
||||
}
|
@ -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"
|
@ -0,0 +1,7 @@
|
||||
package com.simplemobiletools.draw.pro.interfaces
|
||||
|
||||
interface CanvasListener {
|
||||
fun toggleUndoVisibility(visible: Boolean)
|
||||
|
||||
fun toggleRedoVisibility(visible: Boolean)
|
||||
}
|
@ -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>()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)}"
|
||||
}
|
141
app/src/main/kotlin/com/simplemobiletools/draw/pro/models/Svg.kt
Normal file
141
app/src/main/kotlin/com/simplemobiletools/draw/pro/models/Svg.kt
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user