@@ -5,12 +5,14 @@ import android.content.Context
55import android.graphics.Canvas
66import android.graphics.Color
77import android.graphics.Paint
8+ import android.os.Handler
9+ import android.os.Looper
810import android.util.AttributeSet
911import android.view.MotionEvent
10- import android.view.View
1112import android.view.animation.DecelerateInterpolator
1213import android.widget.FrameLayout
13- import java.util.concurrent.ConcurrentHashMap
14+ import java.util.Collections
15+ import java.util.concurrent.CopyOnWriteArrayList
1416
1517/* *
1618 * タッチイベントを視覚的に表示するカスタムビュー
@@ -22,15 +24,21 @@ class TouchVisualizerView @JvmOverloads constructor(
2224 defStyleAttr : Int = 0
2325) : FrameLayout(context, attrs, defStyleAttr) {
2426
25- // タッチポイントを保持するマップ
26- private val touchPoints = ConcurrentHashMap <Int , TouchPoint >()
27+ // タッチポイントを保持するマップ(スレッドセーフ)
28+ private val touchPoints = Collections .synchronizedMap(HashMap <Int , TouchPoint >())
29+
30+ // 描画用のタッチポイントリスト(スレッドセーフ)
31+ private val drawingPoints = CopyOnWriteArrayList <TouchPoint >()
32+
33+ // UIスレッドでの処理用ハンドラ
34+ private val mainHandler = Handler (Looper .getMainLooper())
2735
2836 // タッチポイントの描画用ペイント
2937 private val paint = Paint ().apply {
3038 isAntiAlias = true
3139 color = Color .BLUE
3240 alpha = 128
33- style = Paint .STYLE .FILL
41+ style = Paint .Style .FILL
3442 }
3543
3644 init {
@@ -50,37 +58,55 @@ class TouchVisualizerView @JvmOverloads constructor(
5058
5159 when (event.actionMasked) {
5260 MotionEvent .ACTION_DOWN , MotionEvent .ACTION_POINTER_DOWN -> {
53- // 新しいタッチポイントを追加
54- val touchPoint = TouchPoint (x, y)
55- touchPoints[pointerId] = touchPoint
56- touchPoint.startAnimation()
61+ // 新しいタッチポイントを追加(UIスレッドで実行)
62+ mainHandler.post {
63+ val touchPoint = TouchPoint (x, y)
64+ synchronized(touchPoints) {
65+ touchPoints[pointerId] = touchPoint
66+ // 描画用リストにも追加
67+ drawingPoints.add(touchPoint)
68+ }
69+ touchPoint.startAnimation()
70+ }
5771 }
5872 MotionEvent .ACTION_MOVE -> {
59- // 既存のポインターすべての位置を更新
60- for (i in 0 until event.pointerCount) {
61- val id = event.getPointerId(i)
62- touchPoints[id]?.apply {
63- this .x = event.getX(i)
64- this .y = event.getY(i)
73+ // 既存のポインターすべての位置を更新(UIスレッドで実行)
74+ mainHandler.post {
75+ synchronized(touchPoints) {
76+ for (i in 0 until event.pointerCount) {
77+ val id = event.getPointerId(i)
78+ touchPoints[id]?.apply {
79+ this .x = event.getX(i)
80+ this .y = event.getY(i)
81+ }
82+ }
6583 }
84+ // 再描画を要求
85+ postInvalidateOnAnimation()
6686 }
6787 }
6888 MotionEvent .ACTION_UP , MotionEvent .ACTION_POINTER_UP , MotionEvent .ACTION_CANCEL -> {
69- // タッチポイントをフェードアウト
70- touchPoints[pointerId]?.startFadeOut()
89+ // タッチポイントをフェードアウト(UIスレッドで実行)
90+ mainHandler.post {
91+ synchronized(touchPoints) {
92+ touchPoints[pointerId]?.startFadeOut()
93+ }
94+ }
7195 }
7296 }
7397
74- invalidate()
98+ // 再描画を要求
99+ postInvalidateOnAnimation()
100+
75101 // 子ビューにイベントを渡す
76102 return super .dispatchTouchEvent(event)
77103 }
78104
79105 override fun onDraw (canvas : Canvas ) {
80106 super .onDraw(canvas)
81107
82- // すべてのタッチポイントを描画
83- for (touchPoint in touchPoints.values ) {
108+ // スレッドセーフなリストからすべてのタッチポイントを描画
109+ for (touchPoint in drawingPoints ) {
84110 paint.alpha = touchPoint.alpha
85111 canvas.drawCircle(touchPoint.x, touchPoint.y, touchPoint.radius, paint)
86112 }
@@ -90,10 +116,10 @@ class TouchVisualizerView @JvmOverloads constructor(
90116 * タッチポイントを表すデータクラス
91117 */
92118 private inner class TouchPoint (
93- var x : Float ,
94- var y : Float ,
95- var radius : Float = 0f ,
96- var alpha : Int = 0
119+ @Volatile var x : Float ,
120+ @Volatile var y : Float ,
121+ @Volatile var radius : Float = 0f ,
122+ @Volatile var alpha : Int = 0
97123 ) {
98124 private var animator: ValueAnimator ? = null
99125 private var fadeOutAnimator: ValueAnimator ? = null
@@ -113,7 +139,8 @@ class TouchVisualizerView @JvmOverloads constructor(
113139 addUpdateListener { animation ->
114140 radius = animation.animatedValue as Float
115141 alpha = 128
116- invalidate()
142+ // UIスレッドで再描画を要求
143+ postInvalidateOnAnimation()
117144 }
118145 start()
119146 }
@@ -127,10 +154,21 @@ class TouchVisualizerView @JvmOverloads constructor(
127154 duration = 500
128155 addUpdateListener { animation ->
129156 alpha = animation.animatedValue as Int
130- invalidate()
157+ // UIスレッドで再描画を要求
158+ postInvalidateOnAnimation()
159+
131160 if (alpha == 0 ) {
132- // アニメーション終了時にマップから削除
133- touchPoints.values.removeIf { it == = this @TouchPoint }
161+ // アニメーション終了時にマップから削除(UIスレッドで実行)
162+ mainHandler.post {
163+ synchronized(touchPoints) {
164+ // IDでの削除
165+ val idToRemove = touchPoints.entries.find { it.value == = this @TouchPoint }?.key
166+ idToRemove?.let { touchPoints.remove(it) }
167+
168+ // 描画用リストからも削除
169+ drawingPoints.remove(this @TouchPoint)
170+ }
171+ }
134172 }
135173 }
136174 start()
0 commit comments