Add color filler tool

- Simplified the undo/redo code using `Operation` abstraction

 - Color fill changes is not saved with SVGs. More research/development is needed on this.

 - Bitmap operations are not saved during `onSaveInstanceState` because of the obvious memory/space issue. Perhaps using `Path` objects in the flood-fill algorithm can remedy this and the above issue
This commit is contained in:
Naveen 2022-10-10 04:51:20 +05:30
parent 797cfabcb9
commit 2c4dccdda9
11 changed files with 401 additions and 86 deletions

View File

@ -63,6 +63,7 @@ class MainActivity : SimpleActivity(), CanvasListener {
private var lastSavePromptTS = 0L
private var isEraserOn = false
private var isEyeDropperOn = false
private var isColorFillOn = false
private var isImageCaptureIntent = false
private var isEditIntent = false
private var lastBitmapPath = ""
@ -105,6 +106,7 @@ class MainActivity : SimpleActivity(), CanvasListener {
true
}
color_fill.setOnClickListener { colorFillClicked() }
checkIntents()
if (!isImageCaptureIntent) {
checkWhatsNewDialog()
@ -393,6 +395,20 @@ class MainActivity : SimpleActivity(), CanvasListener {
eye_dropper.setImageResource(iconId)
}
private fun colorFillClicked() {
isColorFillOn = !isColorFillOn
val iconId = if (isColorFillOn) {
R.drawable.ic_color_fill_off_vector
} else {
R.drawable.ic_color_fill_on_vector
}
color_fill.setImageResource(iconId)
my_canvas.toggleColorFill(isColorFillOn)
}
private fun confirmImage() {
when {
isEditIntent -> {
@ -575,6 +591,7 @@ class MainActivity : SimpleActivity(), CanvasListener {
eraser.applyColorFilter(contrastColor)
redo.applyColorFilter(contrastColor)
eye_dropper.applyColorFilter(contrastColor)
color_fill.applyColorFilter(contrastColor)
if (isBlackAndWhiteTheme()) {
stroke_width_bar.setColors(0, contrastColor, 0)
}

View File

@ -0,0 +1,15 @@
package com.simplemobiletools.draw.pro.extensions
import android.graphics.Bitmap
import com.simplemobiletools.draw.pro.helpers.QueueLinearFloodFiller
fun Bitmap.floodFill(color: Int, x: Int, y: Int, tolerance: Int = 10): Bitmap {
val floodFiller = QueueLinearFloodFiller(this).apply {
fillColor = color
setTolerance(tolerance)
}
floodFiller.floodFill(x, y)
return floodFiller.image!!
}

View File

@ -0,0 +1,17 @@
package com.simplemobiletools.draw.pro.extensions
import android.graphics.Rect
import android.view.View
val View.boundingBox: Rect
get() {
val rect = Rect()
getDrawingRect(rect)
val location = IntArray(2)
getLocationOnScreen(location)
rect.offset(location[0], location[1])
return rect
}
fun View.contains(x: Int, y: Int) = boundingBox.contains(x, y)

View File

@ -0,0 +1,167 @@
package com.simplemobiletools.draw.pro.helpers
import android.graphics.Bitmap
import android.graphics.Color
import java.util.*
// Original algorithm by J. Dunlap http:// www.codeproject.com/KB/GDI-plus/queuelinearflood-fill.aspx
// Java port by Owen Kaluza
// Android port by Darrin Smith (Standard Android)
class QueueLinearFloodFiller(img: Bitmap) {
var image: Bitmap? = null
private set
var tolerance = intArrayOf(0, 0, 0)
private var width = 0
private var height = 0
private var pixels: IntArray? = null
var fillColor = 0
private val startColor = intArrayOf(0, 0, 0)
private lateinit var pixelsChecked: BooleanArray
private var ranges: Queue<FloodFillRange>? = null
init {
copyImage(img)
}
fun setTargetColor(targetColor: Int) {
startColor[0] = Color.red(targetColor)
startColor[1] = Color.green(targetColor)
startColor[2] = Color.blue(targetColor)
}
fun setTolerance(value: Int) {
tolerance = intArrayOf(value, value, value)
}
private fun copyImage(img: Bitmap) {
// Copy data from provided Image to a BufferedImage to write flood fill to, use getImage to retrieve
// cache data in member variables to decrease overhead of property calls
width = img.width
height = img.height
image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
image = img.copy(img.config, true)
pixels = IntArray(width * height)
image!!.getPixels(pixels, 0, width, 0, 0, width, height)
}
private fun prepare() {
// Called before starting flood-fill
pixelsChecked = BooleanArray(pixels!!.size)
ranges = LinkedList()
}
// Fills the specified point on the bitmap with the currently selected fill color.
// int x, int y: The starting coordinates for the fill
fun floodFill(x: Int, y: Int) {
// Setup
prepare()
if (startColor[0] == 0) {
// ***Get starting color.
val startPixel = pixels!![width * y + x]
startColor[0] = startPixel shr 16 and 0xff
startColor[1] = startPixel shr 8 and 0xff
startColor[2] = startPixel and 0xff
}
// ***Do first call to flood-fill.
linearFill(x, y)
// ***Call flood-fill routine while flood-fill ranges still exist on the queue
var range: FloodFillRange
while (ranges!!.size > 0) {
// **Get Next Range Off the Queue
range = ranges!!.remove()
// **Check Above and Below Each Pixel in the flood-fill Range
var downPxIdx = width * (range.Y + 1) + range.startX
var upPxIdx = width * (range.Y - 1) + range.startX
val upY = range.Y - 1 // so we can pass the y coordinate by ref
val downY = range.Y + 1
for (i in range.startX..range.endX) {
// *Start Fill Upwards
// if we're not above the top of the bitmap and the pixel above this one is within the color tolerance
if (range.Y > 0 && !pixelsChecked[upPxIdx] && checkPixel(upPxIdx)) {
linearFill(i, upY)
}
// *Start Fill Downwards
// if we're not below the bottom of the bitmap and the pixel below this one is within the color tolerance
if (range.Y < height - 1 && !pixelsChecked[downPxIdx] && checkPixel(downPxIdx)) {
linearFill(i, downY)
}
downPxIdx++
upPxIdx++
}
}
image!!.setPixels(pixels, 0, width, 0, 0, width, height)
}
// Finds the furthermost left and right boundaries of the fill area
// on a given y coordinate, starting from a given x coordinate, filling as it goes.
// Adds the resulting horizontal range to the queue of flood-fill ranges,
// to be processed in the main loop.
//
// int x, int y: The starting coordinates
private fun linearFill(x: Int, y: Int) {
// ***Find Left Edge of Color Area
var lFillLoc = x // the location to check/fill on the left
var pxIdx = width * y + x
while (true) {
// **fill with the color
pixels!![pxIdx] = fillColor
// **indicate that this pixel has already been checked and filled
pixelsChecked[pxIdx] = true
// **de-increment
lFillLoc-- // de-increment counter
pxIdx-- // de-increment pixel index
// **exit loop if we're at edge of bitmap or color area
if (lFillLoc < 0 || pixelsChecked[pxIdx] || !checkPixel(pxIdx)) {
break
}
}
lFillLoc++
// ***Find Right Edge of Color Area
var rFillLoc = x // the location to check/fill on the left
pxIdx = width * y + x
while (true) {
// **fill with the color
pixels!![pxIdx] = fillColor
// **indicate that this pixel has already been checked and filled
pixelsChecked[pxIdx] = true
// **increment
rFillLoc++ // increment counter
pxIdx++ // increment pixel index
// **exit loop if we're at edge of bitmap or color area
if (rFillLoc >= width || pixelsChecked[pxIdx] || !checkPixel(pxIdx)) {
break
}
}
rFillLoc--
// add range to queue
val r = FloodFillRange(lFillLoc, rFillLoc, y)
ranges!!.offer(r)
}
// Sees if a pixel is within the color tolerance range.
private fun checkPixel(px: Int): Boolean {
val red = pixels!![px] ushr 16 and 0xff
val green = pixels!![px] ushr 8 and 0xff
val blue = pixels!![px] and 0xff
return red >= startColor[0] - tolerance[0] && red <= startColor[0] + tolerance[0] && green >= startColor[1] - tolerance[1] && green <= startColor[1] + tolerance[1] && blue >= startColor[2] - tolerance[2] && blue <= startColor[2] + tolerance[2]
}
// Represents a linear range to be filled and branched from.
private inner class FloodFillRange(var startX: Int, var endX: Int, var Y: Int)
}

View File

@ -0,0 +1,16 @@
package com.simplemobiletools.draw.pro.models
import android.graphics.Bitmap
import java.io.Serializable
sealed class CanvasOp : Serializable {
class PathOp(
val path: MyPath,
val paintOptions: PaintOptions
) : CanvasOp()
class BitmapOp(
val bitmap: Bitmap
) : CanvasOp()
}

View File

@ -3,30 +3,37 @@ 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>()
var operations = ArrayList<CanvasOp>()
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
val serializable = parcel.readSerializable()
if (serializable is MyPath) {
val paintOptions = PaintOptions(parcel.readInt(), parcel.readFloat(), parcel.readInt() == 1)
val operation = CanvasOp.PathOp(serializable, paintOptions)
operations.add(operation)
}
}
}
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)
out.writeInt(operations.size)
for (operation in operations) {
if (operation is CanvasOp.PathOp) {
val path = operation.path
val paintOptions = operation.paintOptions
out.writeSerializable(path)
out.writeInt(paintOptions.color)
out.writeFloat(paintOptions.strokeWidth)
out.writeInt(if (paintOptions.isEraser) 1 else 0)
}
}
}

View File

@ -14,7 +14,6 @@ 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) {
@ -27,7 +26,7 @@ object Svg {
if (outputStream != null) {
val backgroundColor = (canvas.background as ColorDrawable).color
val writer = BufferedWriter(OutputStreamWriter(outputStream))
writeSvg(writer, backgroundColor, canvas.mPaths, canvas.width, canvas.height)
writeSvg(writer, backgroundColor, canvas.getPathsMap(), canvas.width, canvas.height)
writer.close()
activity.toast(R.string.file_saved)
} else {

View File

@ -16,7 +16,10 @@ import com.bumptech.glide.request.RequestOptions
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.draw.pro.R
import com.simplemobiletools.draw.pro.extensions.contains
import com.simplemobiletools.draw.pro.extensions.floodFill
import com.simplemobiletools.draw.pro.interfaces.CanvasListener
import com.simplemobiletools.draw.pro.models.CanvasOp
import com.simplemobiletools.draw.pro.models.MyParcelable
import com.simplemobiletools.draw.pro.models.MyPath
import com.simplemobiletools.draw.pro.models.PaintOptions
@ -27,14 +30,16 @@ import kotlin.math.min
class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val MIN_ERASER_WIDTH = 20f
private val MAX_HISTORY_COUNT = 1000
private val BITMAP_MAX_HISTORY_COUNT = 60
private val mScaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
var mPaths = LinkedHashMap<MyPath, PaintOptions>()
private var mOperations = ArrayList<CanvasOp>()
var mBackgroundBitmap: Bitmap? = null
var mListener: CanvasListener? = null
private var mLastPaths = LinkedHashMap<MyPath, PaintOptions>()
private var mUndonePaths = LinkedHashMap<MyPath, PaintOptions>()
private var mUndoneOperations = ArrayList<CanvasOp>()
private var mLastOperations = ArrayList<CanvasOp>()
private var mLastBackgroundBitmap: Bitmap? = null
private var mPaint = Paint()
@ -54,6 +59,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
private var mCurrBrushSize = 0f
private var mAllowMovingZooming = true
private var mIsEraserOn = false
private var mIsColorFillOn = false
private var mWasMultitouch = false
private var mIgnoreTouches = false
private var mWasScalingInGesture = false
@ -78,13 +84,13 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
}
mScaleDetector = ScaleGestureDetector(context, ScaleListener())
pathsUpdated()
updateUndoVisibility()
}
public override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
val savedState = MyParcelable(superState!!)
savedState.paths = mPaths
savedState.operations = mOperations
return savedState
}
@ -95,8 +101,8 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
}
super.onRestoreInstanceState(state.superState)
mPaths = state.paths
pathsUpdated()
mOperations = state.operations
updateUndoVisibility()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
@ -147,8 +153,8 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
mLastTouchX = x
mLastTouchY = y
actionDown(newValueX, newValueY)
mUndonePaths.clear()
mListener?.toggleRedoVisibility(false)
mUndoneOperations.clear()
updateRedoVisibility(false)
}
MotionEvent.ACTION_MOVE -> {
if (mTouchSloppedBeforeMultitouch) {
@ -156,7 +162,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
mTouchSloppedBeforeMultitouch = false
}
if (!mAllowMovingZooming || (!mScaleDetector!!.isInProgress && event.pointerCount == 1 && !mWasMultitouch)) {
if (!mIsColorFillOn && (!mAllowMovingZooming || (!mScaleDetector!!.isInProgress && event.pointerCount == 1 && !mWasMultitouch))) {
actionMove(newValueX, newValueY)
}
@ -207,14 +213,27 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
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)
val bitmap = mBackgroundBitmap!!
val left = (width - bitmap.width) / 2f
val top = (height - bitmap.height) / 2f
canvas.drawBitmap(bitmap, left, top, null)
}
for ((key, value) in mPaths) {
changePaint(value)
canvas.drawPath(key, mPaint)
if (mOperations.isNotEmpty()) {
val bitmapOps = mOperations.filterIsInstance<CanvasOp.BitmapOp>()
val bitmapOp = bitmapOps.lastOrNull()
if (bitmapOp != null) {
canvas.drawBitmap(bitmapOp.bitmap, 0f, 0f, null)
}
// only perform path ops after last bitmap op as any previous path operations are already visible due to the bitmap op
val startIndex = if (bitmapOp != null) mOperations.indexOf(bitmapOp) else 0
val endIndex = mOperations.lastIndex
val pathOps = mOperations.slice(startIndex..endIndex).filterIsInstance<CanvasOp.PathOp>()
for (pathOp in pathOps) {
changePaint(pathOp.paintOptions)
canvas.drawPath(pathOp.path, mPaint)
}
}
changePaint(mPaintOptions)
@ -223,44 +242,30 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
}
fun undo() {
if (mPaths.isEmpty() && mLastPaths.isNotEmpty()) {
mPaths = mLastPaths.clone() as LinkedHashMap<MyPath, PaintOptions>
if (mOperations.isEmpty() && mLastOperations.isNotEmpty()) {
mOperations = mLastOperations.clone() as ArrayList<CanvasOp>
mBackgroundBitmap = mLastBackgroundBitmap
mLastPaths.clear()
pathsUpdated()
mLastOperations.clear()
updateUndoVisibility()
invalidate()
return
}
if (mPaths.isEmpty()) {
return
if (mOperations.isNotEmpty()) {
val lastOp = mOperations.removeLast()
mUndoneOperations.add(lastOp)
invalidate()
}
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()
updateUndoRedoVisibility()
}
fun redo() {
if (mUndonePaths.keys.isEmpty()) {
mListener?.toggleRedoVisibility(false)
return
if (mUndoneOperations.isNotEmpty()) {
val undoneOperation = mUndoneOperations.removeLast()
addOperation(undoneOperation)
invalidate()
}
val lastKey = mUndonePaths.keys.last()
addPath(lastKey, mUndonePaths.values.last())
mUndonePaths.remove(lastKey)
if (mUndonePaths.isEmpty()) {
mListener?.toggleRedoVisibility(false)
}
invalidate()
updateUndoRedoVisibility()
}
fun toggleEraser(isEraserOn: Boolean) {
@ -269,6 +274,10 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
invalidate()
}
fun toggleColorFill(isColorFillOn: Boolean) {
mIsColorFillOn = isColorFillOn
}
fun setColor(newColor: Int) {
mPaintOptions.color = newColor
}
@ -300,17 +309,10 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
ensureBackgroundThread {
val size = Point()
activity.windowManager.defaultDisplay.getSize(size)
val options = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.disallowHardwareConfig()
.fitCenter()
val options = RequestOptions().format(DecodeFormat.PREFER_ARGB_8888).disallowHardwareConfig().fitCenter()
try {
val builder = Glide.with(context)
.asBitmap()
.load(path)
.apply(options)
.submit(size.x, size.y)
val builder = Glide.with(context).asBitmap().load(path).apply(options).submit(size.x, size.y)
mBackgroundBitmap = builder.get()
activity.runOnUiThread {
@ -324,8 +326,9 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
}
fun addPath(path: MyPath, options: PaintOptions) {
mPaths[path] = options
pathsUpdated()
val pathOp = CanvasOp.PathOp(path, options)
mOperations.add(pathOp)
updateUndoVisibility()
}
private fun changePaint(paintOptions: PaintOptions) {
@ -337,12 +340,12 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
}
fun clearCanvas() {
mLastPaths = mPaths.clone() as LinkedHashMap<MyPath, PaintOptions>
mLastOperations = mOperations.clone() as ArrayList<CanvasOp>
mLastBackgroundBitmap = mBackgroundBitmap
mBackgroundBitmap = null
mPath.reset()
mPaths.clear()
pathsUpdated()
mOperations.clear()
updateUndoVisibility()
invalidate()
}
@ -360,28 +363,84 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
}
private fun actionUp(forceLineDraw: Boolean) {
if (!mWasMultitouch || forceLineDraw) {
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
if (mIsColorFillOn) {
colorFill()
} else if (!mWasMultitouch || forceLineDraw) {
drawADot()
}
pathsUpdated()
updateUndoVisibility()
mPath = MyPath()
mPaintOptions = PaintOptions(mPaintOptions.color, mPaintOptions.strokeWidth, mPaintOptions.isEraser)
}
private fun pathsUpdated() {
mListener?.toggleUndoVisibility(mPaths.isNotEmpty() || mLastPaths.isNotEmpty())
private fun updateUndoRedoVisibility() {
updateUndoVisibility()
updateRedoVisibility()
}
fun getDrawingHashCode() = mPaths.hashCode().toLong() + (mBackgroundBitmap?.hashCode()?.toLong() ?: 0L)
private fun updateUndoVisibility() {
mListener?.toggleUndoVisibility(mOperations.isNotEmpty() || mLastOperations.isNotEmpty())
}
private fun updateRedoVisibility(visible: Boolean = mUndoneOperations.isNotEmpty()) {
mListener?.toggleRedoVisibility(visible)
}
private fun colorFill() {
val touchedX = mCurX.toInt()
val touchedY = mCurY.toInt()
if (contains(touchedX, touchedY)) {
val bitmap = getBitmap()
val color = mPaintOptions.color
val img = bitmap.floodFill(color = color, x = touchedX, y = touchedY)
addOperation(CanvasOp.BitmapOp(img))
invalidate()
}
}
private fun drawADot() {
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)
}
mOperations.add(CanvasOp.PathOp(mPath, mPaintOptions))
}
private fun addOperation(operation: CanvasOp) {
mOperations.add(operation)
// maybe free up some memory
while (mOperations.size > MAX_HISTORY_COUNT) {
val item = mOperations.removeFirst()
if (item is CanvasOp.BitmapOp) {
item.bitmap.recycle()
}
}
val ops = mOperations.filterIsInstance<CanvasOp.BitmapOp>()
if (ops.size > BITMAP_MAX_HISTORY_COUNT) {
val start = ops.lastIndex - BITMAP_MAX_HISTORY_COUNT
val bitmapOp = ops.slice(start..ops.lastIndex).first()
val startIndex = mOperations.indexOf(bitmapOp)
mOperations = mOperations.slice(startIndex..mOperations.lastIndex) as ArrayList<CanvasOp>
}
}
fun getPathsMap(): Map<MyPath, PaintOptions> {
val pathOps = mOperations
.filterIsInstance<CanvasOp.PathOp>()
.map { it.path to it.paintOptions }
.toTypedArray()
return mapOf(*pathOps)
}
fun getDrawingHashCode() = mOperations.hashCode().toLong() + (mBackgroundBitmap?.hashCode()?.toLong() ?: 0L)
private fun MotionEvent?.isTouchSlop(pointerIndex: Int, startX: Float, startY: Float): Boolean {
return if (this == null || actionMasked != MotionEvent.ACTION_MOVE) {

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,14c0,-4 -6,-10.8 -6,-10.8s-1.33,1.51 -2.73,3.52l8.59,8.59c0.09,-0.42 0.14,-0.86 0.14,-1.31zM17.12,17.12L12.5,12.5 5.27,5.27 4,6.55l3.32,3.32C6.55,11.32 6,12.79 6,14c0,3.31 2.69,6 6,6 1.52,0 2.9,-0.57 3.96,-1.5l2.63,2.63 1.27,-1.27 -2.74,-2.74z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:viewportHeight="48"
android:viewportWidth="48" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF"
android:pathData="M10.561,5.804 L13.074,3.079 33.411,25.136c0.896,0.972 1.344,2.166 1.344,3.581 0,1.416 -0.448,2.609 -1.344,3.581L23.009,43.58c-0.896,0.972 -1.948,1.458 -3.156,1.458 -1.208,0 -2.26,-0.486 -3.156,-1.458L6.295,32.298c-0.896,-0.972 -1.344,-2.166 -1.344,-3.581 0,-1.416 0.448,-2.609 1.344,-3.581L17.341,13.157ZM19.853,15.882 L8.224,28.495L31.483,28.495ZM39.606,46.116c-1.169,0 -2.162,-0.444 -2.98,-1.331 -0.818,-0.887 -1.227,-1.965 -1.227,-3.233 0,-0.718 0.156,-1.5 0.468,-2.345 0.312,-0.845 0.74,-1.711 1.286,-2.599 0.312,-0.549 0.692,-1.141 1.14,-1.775 0.448,-0.634 0.886,-1.225 1.315,-1.775 0.429,0.549 0.867,1.141 1.315,1.775 0.448,0.634 0.828,1.225 1.14,1.775 0.545,0.887 0.974,1.754 1.286,2.599 0.312,0.845 0.468,1.627 0.468,2.345 0,1.268 -0.409,2.345 -1.227,3.233 -0.818,0.887 -1.812,1.331 -2.98,1.331z" android:strokeWidth="1.21722"/>
</vector>

View File

@ -44,11 +44,19 @@
android:id="@+id/redo"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_toStartOf="@+id/eraser"
android:layout_toStartOf="@+id/color_fill"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_redo_vector"
android:visibility="gone" />
<ImageView
android:id="@+id/color_fill"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_toStartOf="@+id/eraser"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_color_fill_on_vector" />
<ImageView
android:id="@+id/eraser"
android:layout_width="@dimen/normal_icon_size"