diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d07ba820..ee49ffc9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("kotlin-kapt") } android { diff --git a/app/src/main/java/woowacourse/paint/MainActivity.kt b/app/src/main/java/woowacourse/paint/MainActivity.kt index 4da7b3a7..0192f1b0 100644 --- a/app/src/main/java/woowacourse/paint/MainActivity.kt +++ b/app/src/main/java/woowacourse/paint/MainActivity.kt @@ -4,7 +4,10 @@ import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.google.android.material.slider.RangeSlider +import woowacourse.paint.canvas.DrawingTool import woowacourse.paint.databinding.ActivityMainBinding +import woowacourse.paint.databinding.ItemToolBinding +import woowacourse.paint.utils.toUiModel class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding @@ -13,12 +16,15 @@ class MainActivity : AppCompatActivity() { private val adapter = ColorsAdapter { model -> viewModel.pickColor(model) } + private val toolButtons = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupBinding() setupViewModel() + setupToolbar() setupCanvas() + setupTools() setupColors() setupWidthSlider() } @@ -40,6 +46,42 @@ class MainActivity : AppCompatActivity() { adapter.submitList(colors) } } + viewModel.selectedDrawingTool.observe(this) { tool -> + canvasView.setupTools(tool) + changeButtonSelectedStatus(tool) + } + } + + private fun changeButtonSelectedStatus(drawingTool: DrawingTool) { + toolButtons.forEach { button -> + button.root.isSelected = button.drawingTool == drawingTool + } + viewModel.setSettingState(PaintChangingState.NOTHING) + } + + private fun setupToolbar() { + setSupportActionBar(binding.tbPaint) + binding.ivRedo.setOnClickListener { + canvasView.redo() + } + binding.ivUndo.setOnClickListener { + canvasView.undo() + } + binding.ivClear.setOnClickListener { + canvasView.eraseAll() + } + } + + private fun setupTools() { + viewModel.drawingTools.forEach { toolAssigned -> + val toolButtonBinding = ItemToolBinding.inflate(layoutInflater, binding.llTools, true) + with(toolButtonBinding) { + onClick = viewModel::pickTool + drawingTool = toolAssigned + name = getString(toolAssigned.toUiModel().toolNameId) + } + toolButtons.add(toolButtonBinding) + } } private fun setupCanvas() { @@ -67,17 +109,13 @@ class MainActivity : AppCompatActivity() { } private fun setupWidthSlider() { - binding.rsThicknessChanger.valueFrom = minWidth - binding.rsThicknessChanger.valueTo = maxWidth - binding.rsThicknessChanger.addOnChangeListener( + binding.rsWidthChanger.valueFrom = MainViewModel.MIN_WIDTH + binding.rsWidthChanger.valueTo = MainViewModel.MAX_WIDTH + binding.rsWidthChanger.setValues(viewModel.width.value) + binding.rsWidthChanger.addOnChangeListener( RangeSlider.OnChangeListener { _, value, _ -> viewModel.pickWidth(value) }, ) } - - companion object { - private const val minWidth = 0f - private const val maxWidth = 100f - } } diff --git a/app/src/main/java/woowacourse/paint/MainViewModel.kt b/app/src/main/java/woowacourse/paint/MainViewModel.kt index 4f3481e1..c79af3df 100644 --- a/app/src/main/java/woowacourse/paint/MainViewModel.kt +++ b/app/src/main/java/woowacourse/paint/MainViewModel.kt @@ -4,12 +4,13 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel +import woowacourse.paint.canvas.DrawingTool import woowacourse.paint.canvas.PaletteColor import woowacourse.paint.model.ColorUiModel class MainViewModel : ViewModel() { private val _paintChangingState = - MutableLiveData(PaintChangingState.Nothing) + MutableLiveData(PaintChangingState.NOTHING) val paintChangingState: LiveData get() = _paintChangingState @@ -18,6 +19,12 @@ class MainViewModel : ViewModel() { val colors: LiveData> get() = _colors + val drawingTools = DrawingTool.values().toList() + + private var _selectedTool = MutableLiveData(drawingTools.first()) + val selectedDrawingTool: LiveData + get() = _selectedTool + val selectedColor: LiveData get() = Transformations.map(_colors) { colors -> colors.firstOrNull { it.isPicked }?.color ?: DEFAULT_SELECTED_COLOR @@ -27,20 +34,16 @@ class MainViewModel : ViewModel() { val width: LiveData get() = _width - fun setColorSettingState() { - if (_paintChangingState.value == PaintChangingState.ColorChanging) { - _paintChangingState.value = PaintChangingState.Nothing + fun setSettingState(state: PaintChangingState) { + if (_paintChangingState.value == state) { + _paintChangingState.value = PaintChangingState.NOTHING return } - _paintChangingState.value = PaintChangingState.ColorChanging + _paintChangingState.value = state } - fun setWidthSettingState() { - if (_paintChangingState.value == PaintChangingState.WidthChanging) { - _paintChangingState.value = PaintChangingState.Nothing - return - } - _paintChangingState.value = PaintChangingState.WidthChanging + fun pickTool(drawingTool: DrawingTool) { + _selectedTool.value = drawingTool } fun pickColor(model: ColorUiModel) { @@ -53,7 +56,9 @@ class MainViewModel : ViewModel() { } companion object { - const val DEFAULT_WIDTH = 0F + const val DEFAULT_WIDTH = 1F + const val MIN_WIDTH = 1f + const val MAX_WIDTH = 100f val DEFAULT_SELECTED_COLOR = PaletteColor.RED } } diff --git a/app/src/main/java/woowacourse/paint/PaintChangingState.kt b/app/src/main/java/woowacourse/paint/PaintChangingState.kt index bb45f621..394e7c98 100644 --- a/app/src/main/java/woowacourse/paint/PaintChangingState.kt +++ b/app/src/main/java/woowacourse/paint/PaintChangingState.kt @@ -1,7 +1,5 @@ package woowacourse.paint -sealed class PaintChangingState { - object Nothing : PaintChangingState() - object ColorChanging : PaintChangingState() - object WidthChanging : PaintChangingState() +enum class PaintChangingState { + NOTHING, COLOR_CHANGING, WIDTH_CHANGING } diff --git a/app/src/main/java/woowacourse/paint/canvas/CanvasView.kt b/app/src/main/java/woowacourse/paint/canvas/CanvasView.kt index cab03df0..c3a7de46 100644 --- a/app/src/main/java/woowacourse/paint/canvas/CanvasView.kt +++ b/app/src/main/java/woowacourse/paint/canvas/CanvasView.kt @@ -3,83 +3,82 @@ package woowacourse.paint.canvas import android.content.Context import android.graphics.Canvas import android.graphics.Paint -import android.graphics.Path +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode import android.util.AttributeSet import android.view.MotionEvent import android.view.View -import androidx.annotation.ColorInt +import woowacourse.paint.canvas.drawing.Drawings class CanvasView(context: Context, attr: AttributeSet) : View( context, attr, ) { - private var path = Path() - private var paint = Paint() - private var startPoint: Point = Point(0f, 0f) - private val lines = mutableListOf() + private var drawingTool = DrawingTool.PEN + private val paint = Paint().apply { + isAntiAlias = true + style = Paint.Style.FILL_AND_STROKE + strokeCap = Paint.Cap.ROUND + xfermode = if (drawingTool == DrawingTool.ERASER) { + PorterDuffXfermode(PorterDuff.Mode.CLEAR) + } else { + null + } + } + private val drawings = Drawings() - fun initPaint(width: Float, color: PaletteColor) { - paint = getPaint(width, color.colorCode) + init { + setLayerType(LAYER_TYPE_HARDWARE, null) + } + + fun initPaint(width: Float, selectedColor: PaletteColor) { + setupWidth(width) + setupColor(selectedColor) } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - lines.forEach { line -> - canvas.drawPath(line.path, line.paint) - } - canvas.drawPath(path, paint) + drawings.drawAll(canvas) } override fun onTouchEvent(event: MotionEvent): Boolean { - val x = event.x - val y = event.y - when (event.action) { - MotionEvent.ACTION_DOWN -> drawDot(x, y) - MotionEvent.ACTION_MOVE -> drawLine(x, y) + MotionEvent.ACTION_DOWN -> drawings.add(drawingTool.draw(paint, ::invalidate)) + MotionEvent.ACTION_UP -> drawings.checkLastDrawingEmpty() else -> super.onTouchEvent(event) } - return true - } - - private fun drawDot(x: Float, y: Float) { - startPoint = Point(x, y) - path.moveTo(startPoint.x, startPoint.y) - path.lineTo(x, y) - invalidate() + return drawings.onDrawingTouchEvent(event) } - private fun drawLine(x: Float, y: Float) { - path.moveTo(startPoint.x, startPoint.y) - path.lineTo(x, y) - startPoint = Point(x, y) - invalidate() + fun setupTools(selectedDrawingTool: DrawingTool) { + drawingTool = selectedDrawingTool + if (drawingTool == DrawingTool.ERASER) { + paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + return + } + paint.xfermode = null } fun setupWidth(width: Float) { - addLine() - paint = getPaint(width, paint.color) + paint.strokeWidth = width } fun setupColor(color: PaletteColor) { - addLine() - paint = getPaint(paint.strokeWidth, color.colorCode) + paint.color = color.colorCode } - private fun addLine() { - if (path.isEmpty) return - lines.add(Line(path, paint)) - path = Path() + fun eraseAll() { + drawings.clear() + invalidate() } - private fun getPaint(width: Float, @ColorInt selectedColor: Int): Paint { - return Paint().apply { - isAntiAlias = true - style = Paint.Style.STROKE - strokeJoin = Paint.Join.ROUND - color = selectedColor - strokeWidth = width - strokeCap = Paint.Cap.ROUND - } + fun undo() { + drawings.undo() + invalidate() + } + + fun redo() { + drawings.redo() + invalidate() } } diff --git a/app/src/main/java/woowacourse/paint/canvas/DrawingTool.kt b/app/src/main/java/woowacourse/paint/canvas/DrawingTool.kt new file mode 100644 index 00000000..07303c07 --- /dev/null +++ b/app/src/main/java/woowacourse/paint/canvas/DrawingTool.kt @@ -0,0 +1,28 @@ +package woowacourse.paint.canvas + +import android.graphics.Paint +import woowacourse.paint.canvas.drawing.Circle +import woowacourse.paint.canvas.drawing.Drawing +import woowacourse.paint.canvas.drawing.Line +import woowacourse.paint.canvas.drawing.Rectangle + +enum class DrawingTool(val hasWidth: Boolean, val hasColor: Boolean) { + PEN(true, true) { + override fun draw(paint: Paint, invalidate: () -> Unit): Drawing = + Line(paint, invalidate) + }, + RECTANGLE(false, true) { + override fun draw(paint: Paint, invalidate: () -> Unit): Drawing = + Rectangle(paint, invalidate) + }, + CIRCLE(false, true) { + override fun draw(paint: Paint, invalidate: () -> Unit): Drawing = + Circle(paint, invalidate) + }, + ERASER(true, false) { + override fun draw(paint: Paint, invalidate: () -> Unit): Drawing = + Line(paint, invalidate) + }, ; + + abstract fun draw(paint: Paint, invalidate: () -> Unit): Drawing +} diff --git a/app/src/main/java/woowacourse/paint/canvas/Line.kt b/app/src/main/java/woowacourse/paint/canvas/Line.kt deleted file mode 100644 index a1e0d5c5..00000000 --- a/app/src/main/java/woowacourse/paint/canvas/Line.kt +++ /dev/null @@ -1,6 +0,0 @@ -package woowacourse.paint.canvas - -import android.graphics.Paint -import android.graphics.Path - -class Line(val path: Path, val paint: Paint) diff --git a/app/src/main/java/woowacourse/paint/canvas/drawing/Circle.kt b/app/src/main/java/woowacourse/paint/canvas/drawing/Circle.kt new file mode 100644 index 00000000..2550a55e --- /dev/null +++ b/app/src/main/java/woowacourse/paint/canvas/drawing/Circle.kt @@ -0,0 +1,28 @@ +package woowacourse.paint.canvas.drawing + +import android.graphics.Paint +import android.graphics.Path +import android.view.MotionEvent +import woowacourse.paint.canvas.Point + +class Circle(paint: Paint, private val invalidate: () -> Unit) : + Drawing(Paint(paint)) { + private lateinit var startPoint: Point + + override fun onTouchEvent(event: MotionEvent): Boolean { + val x = event.x + val y = event.y + + when (event.action) { + MotionEvent.ACTION_DOWN -> startPoint = Point(x, y) + MotionEvent.ACTION_MOVE -> { + path.reset() + path.addOval(startPoint.x, startPoint.y, x, y, Path.Direction.CW) + invalidate() + } + + else -> return true + } + return true + } +} diff --git a/app/src/main/java/woowacourse/paint/canvas/drawing/Drawing.kt b/app/src/main/java/woowacourse/paint/canvas/drawing/Drawing.kt new file mode 100644 index 00000000..bb7e495f --- /dev/null +++ b/app/src/main/java/woowacourse/paint/canvas/drawing/Drawing.kt @@ -0,0 +1,15 @@ +package woowacourse.paint.canvas.drawing + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.view.MotionEvent + +abstract class Drawing(val paint: Paint) { + val path = Path() + fun onDraw(canvas: Canvas) { + canvas.drawPath(path, paint) + } + + abstract fun onTouchEvent(event: MotionEvent): Boolean +} diff --git a/app/src/main/java/woowacourse/paint/canvas/drawing/Drawings.kt b/app/src/main/java/woowacourse/paint/canvas/drawing/Drawings.kt new file mode 100644 index 00000000..5f4fe87b --- /dev/null +++ b/app/src/main/java/woowacourse/paint/canvas/drawing/Drawings.kt @@ -0,0 +1,48 @@ +package woowacourse.paint.canvas.drawing + +import android.graphics.Canvas +import android.view.MotionEvent + +class Drawings( + private val drawings: MutableList = mutableListOf(), + private val drawingsCanceled: MutableList = mutableListOf(), +) { + + fun onDrawingTouchEvent(event: MotionEvent): Boolean { + return drawings.last().onTouchEvent(event) + } + + fun drawAll(canvas: Canvas) { + drawings.forEach { drawing -> + drawing.onDraw(canvas) + } + } + + fun add(drawing: Drawing) { + drawingsCanceled.clear() + drawings.add(drawing) + } + + fun checkLastDrawingEmpty() { + drawings.lastOrNull()?.let { drawing -> + if (drawing.path.isEmpty) drawings.remove(drawing) + } + } + + fun undo() { + drawings.removeLastOrNull()?.let { drawing -> + drawingsCanceled.add(drawing) + } + } + + fun redo() { + drawingsCanceled.removeLastOrNull()?.let { drawing -> + drawings.add(drawing) + } + } + + fun clear() { + drawings.clear() + drawingsCanceled.clear() + } +} diff --git a/app/src/main/java/woowacourse/paint/canvas/drawing/Line.kt b/app/src/main/java/woowacourse/paint/canvas/drawing/Line.kt new file mode 100644 index 00000000..b6b93288 --- /dev/null +++ b/app/src/main/java/woowacourse/paint/canvas/drawing/Line.kt @@ -0,0 +1,33 @@ +package woowacourse.paint.canvas.drawing + +import android.graphics.Paint +import android.view.MotionEvent +import woowacourse.paint.canvas.Point + +class Line(paint: Paint, val invalidate: () -> Unit) : Drawing(Paint(paint)) { + private lateinit var startPoint: Point + + override fun onTouchEvent(event: MotionEvent): Boolean { + val x = event.x + val y = event.y + + when (event.action) { + MotionEvent.ACTION_DOWN -> { + startPoint = Point(x, y) + path.moveTo(startPoint.x, startPoint.y) + path.lineTo(x, y) + invalidate() + } + + MotionEvent.ACTION_MOVE -> { + path.moveTo(startPoint.x, startPoint.y) + path.lineTo(x, y) + startPoint = Point(x, y) + invalidate() + } + + else -> return true + } + return true + } +} diff --git a/app/src/main/java/woowacourse/paint/canvas/drawing/Rectangle.kt b/app/src/main/java/woowacourse/paint/canvas/drawing/Rectangle.kt new file mode 100644 index 00000000..35891b68 --- /dev/null +++ b/app/src/main/java/woowacourse/paint/canvas/drawing/Rectangle.kt @@ -0,0 +1,33 @@ +package woowacourse.paint.canvas.drawing + +import android.graphics.Paint +import android.graphics.Path +import android.view.MotionEvent +import woowacourse.paint.canvas.Point + +class Rectangle(paint: Paint, private val invalidate: () -> Unit) : + Drawing(Paint(paint)) { + private lateinit var startPoint: Point + + override fun onTouchEvent(event: MotionEvent): Boolean { + val x = event.x + val y = event.y + + when (event.action) { + MotionEvent.ACTION_DOWN -> startPoint = Point(x, y) + MotionEvent.ACTION_MOVE -> { + val left = if (startPoint.x < x) startPoint.x else x + val top = if (startPoint.y > y) y else startPoint.y + val right = if (startPoint.x < x) x else startPoint.x + val bottom = if (startPoint.y > y) startPoint.y else y + + path.reset() + path.addRect(left, top, right, bottom, Path.Direction.CW) + invalidate() + } + + else -> return true + } + return true + } +} diff --git a/app/src/main/java/woowacourse/paint/model/DrawingToolUiModel.kt b/app/src/main/java/woowacourse/paint/model/DrawingToolUiModel.kt new file mode 100644 index 00000000..96c37f44 --- /dev/null +++ b/app/src/main/java/woowacourse/paint/model/DrawingToolUiModel.kt @@ -0,0 +1,5 @@ +package woowacourse.paint.model + +import androidx.annotation.StringRes + +data class DrawingToolUiModel(@StringRes val toolNameId: Int) diff --git a/app/src/main/java/woowacourse/paint/utils/DrawingToolMapper.kt b/app/src/main/java/woowacourse/paint/utils/DrawingToolMapper.kt new file mode 100644 index 00000000..c69eced5 --- /dev/null +++ b/app/src/main/java/woowacourse/paint/utils/DrawingToolMapper.kt @@ -0,0 +1,14 @@ +package woowacourse.paint.utils + +import woowacourse.paint.R +import woowacourse.paint.canvas.DrawingTool +import woowacourse.paint.model.DrawingToolUiModel + +fun DrawingTool.toUiModel(): DrawingToolUiModel { + return when (this) { + DrawingTool.PEN -> DrawingToolUiModel(R.string.pen) + DrawingTool.RECTANGLE -> DrawingToolUiModel(R.string.rectangle) + DrawingTool.CIRCLE -> DrawingToolUiModel(R.string.circle) + DrawingTool.ERASER -> DrawingToolUiModel(R.string.eraser) + } +} diff --git a/app/src/main/res/color/brush_selected.xml b/app/src/main/res/color/brush_selected.xml new file mode 100644 index 00000000..ee3d6ff0 --- /dev/null +++ b/app/src/main/res/color/brush_selected.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_clear_24.xml b/app/src/main/res/drawable/ic_clear_24.xml new file mode 100644 index 00000000..ff31d2ae --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_redo_24.xml b/app/src/main/res/drawable/ic_redo_24.xml new file mode 100644 index 00000000..dcd6cffc --- /dev/null +++ b/app/src/main/res/drawable/ic_redo_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_undo_24.xml b/app/src/main/res/drawable/ic_undo_24.xml new file mode 100644 index 00000000..077d163c --- /dev/null +++ b/app/src/main/res/drawable/ic_undo_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 11529f90..5a71a222 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -9,6 +9,8 @@ + + @@ -18,21 +20,60 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/tb_paint" /> @@ -41,37 +82,46 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="horizontal" - android:visibility="@{viewModel.paintChangingState instanceof PaintChangingState.ColorChanging ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.paintChangingState == PaintChangingState.COLOR_CHANGING && viewModel.selectedDrawingTool.hasColor ? View.VISIBLE : View.GONE}" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + app:layout_constraintBottom_toTopOf="@id/btn_change_color" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/rs_thickness_changer" tools:listitem="@layout/item_color_palette" />