스타일러스펜(SPen)으로 드로잉하는거 대충 테스트해보다가 기억할겸 정리한다. 설명은 간단히 주석으로 달았다.
팜리젝션을 위해 일반 터치 이벤트는 무시하고 SPen 만 인식하도록 구현했다.
터치 이벤트가 발생한 좌표에 그리기, 펜에 있는 버튼을 누른 채 문지르면 지워진다. (획단위 아님)
풀 코드
class DrawingView(context: Context, attributeSet: AttributeSet) : View(context) {
init {
// 아래와 같이 지정해주지 않으면 지우개모드에서 이상한 검정색 원이 생긴다.
// 참고로 기본 동작은 LAYER_TYPE_SOFTWARE 인데 자세한 내용은 찾아봐야한다.
setLayerType(FrameLayout.LAYER_TYPE_HARDWARE, null)
}
companion object {
private const val ERASER_SIZE = 20F
private const val TOUCH_TOLERANCE = 2f
// event.buttonState == MotionEvent.BUTTON_STYLUS_PRIMARY 일 때 각 액션이 아래로 치환되어 내려온다.
// 이유는 모르겠다.
private const val SPEN_ACTION_DOWN = 211
private const val SPEN_ACTION_UP = 212
private const val SPEN_ACTION_MOVE = 213
}
private var strokePoint = PointF(0F, 0F)
private val strokePath = Path()
// 드로잉 펜 설정
private val strokePaint = Paint().apply {
isAntiAlias = true
isDither = true
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
strokeWidth = convertDpToPixel(2F)
}
// 지우개 모드일 때 지워지는 영역을 표시하기 위한 설정
private val eraserCirclePaint = Paint().apply {
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = convertDpToPixel(1F)
}
// 지우개 설정
private val eraserPaint = Paint().apply {
isAntiAlias = true
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
private var isMoving = false
private var isErasing = false
private var lastEraserPositionX = 0F
private var lastEraserPositionY = 0F
private var scribeCanvasBitmap: Bitmap? = null
private lateinit var scribeCanvas: Canvas
private var canvasWidth = -1
private var canvasHeight = -1
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
canvasWidth = w
canvasHeight = h
scribeCanvasBitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888)
scribeCanvasBitmap?.let {
scribeCanvas = Canvas(it)
}
}
override fun onDraw(canvas: Canvas?) {
// 그린 path 를 비트맵 객체에 중간 저장, 다시 캔버스에 그리는 부분이라고 보면 된다.
scribeCanvasBitmap?.let { canvas?.drawBitmap(it, 0F, 0F, strokePaint) }
canvas?.drawPath(strokePath, if (isErasing) eraserPaint else strokePaint)
if (isErasing && isMoving) {
canvas?.drawCircle(lastEraserPositionX, lastEraserPositionY, ERASER_SIZE, eraserCirclePaint)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event == null) return false
// 스타일러스 펜 터치가 아닐 경우 터치 이벤트를 처리하지 않는다.
// 아마도 멀티 터치일 경우 무조건 0으로 하면 안될 것
if (event.getToolType(0) != MotionEvent.TOOL_TYPE_STYLUS) return false
val touchX = event.x
val touchY = event.y
if (event.buttonState == MotionEvent.BUTTON_STYLUS_PRIMARY) {
// 화면에서 손을 떼지 않는 이상 드로잉/지우개 모드 전환을 하지 않을 것(대부분 앱이 그렇게 동작하길래..)
if (!isMoving) {
isErasing = !isErasing
}
}
when (event.action) {
MotionEvent.ACTION_DOWN, SPEN_ACTION_DOWN -> {
isMoving = true
strokePath.reset()
strokePath.moveTo(touchX, touchY)
if (isErasing) {
scribeCanvas.drawCircle(touchX, touchY, ERASER_SIZE, eraserPaint)
lastEraserPositionX = touchX
lastEraserPositionY = touchY
} else {
strokePoint = PointF(touchX, touchY)
}
invalidate()
}
MotionEvent.ACTION_MOVE, SPEN_ACTION_MOVE -> {
if (isErasing) {
strokePath.addCircle(touchX, touchY, ERASER_SIZE, Path.Direction.CW)
lastEraserPositionX = touchX
lastEraserPositionY = touchY
} else {
val dx = abs(touchX - strokePoint.x)
val dy = abs(touchY - strokePoint.y)
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
strokePath.quadTo(
strokePoint.x,
strokePoint.y,
(touchX + strokePoint.x) / 2,
(touchY + strokePoint.y) / 2
)
strokePoint = PointF(touchX, touchY)
}
}
invalidate()
}
MotionEvent.ACTION_UP, SPEN_ACTION_UP -> {
if (isErasing) {
scribeCanvas.drawPath(strokePath, eraserPaint)
} else {
scribeCanvas.drawPath(strokePath, strokePaint)
}
isMoving = false
isErasing = false
lastEraserPositionX = 0F
lastEraserPositionY = 0F
strokePath.reset()
invalidate()
}
}
return true
}
private fun convertDpToPixel(dp: Float): Float {
return if (context != null) {
val resources = context.resources
val metrics = resources.displayMetrics
dp * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
} else {
val metrics = Resources.getSystem().displayMetrics
dp * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
}
}
}
추가로 했던 삽질 공유 ㅠㅠ
펜 버튼을 누른 상태에서 지우다가(ACTION_MOVE 유지), 버튼 떼고 계속 문지르면 지우개 모양 잔상이 남는다(ACTION_UP 처리가 안된다). 액션을 찍어보니 event.buttonState == MotionEvent.BUTTON_STYLUS_PRIMARY
는 false 인데도, event.action 이 원래 값으로 내려오지 않기 때문이다.
결론은 event.buttonState
은 MotionEvent.action
값에 영향을 끼치는데, 화면에서 손을 떼지 않은 경우 buttonState 가 아무리 변경되어도 영향을 끼친 그 상태를 유지한다.
'Android > 지식저장소' 카테고리의 다른 글
[Android] ExoPlayer2 현재 재생위치 가져오기 (0) | 2021.10.01 |
---|---|
[Android] ConstraintLayout Helper - Group (0) | 2021.01.11 |
[Android] 점선 그리기(Dotted line) (0) | 2021.01.04 |
[Android] BaseSavedState 를 이용하여 View 에 상태 저장하기 (0) | 2020.09.15 |
[Android] RxJava2 + Retrofit2 에서 언제 call 이 cancel 되는가 고찰 (0) | 2020.09.10 |