39 private static final int SCROLL_TICK_MS = 16;
40 private static final float SCROLL_DEADZONE_DP = 8f;
41 private static final float SCROLL_NPS_PER_DP = 0.6f;
42 private static final float SCROLL_MAX_NPS = 25f;
43 private static final int LONG_PRESS_MS = 500;
46 private ImageView cursor;
50 private float density;
51 private int touchSlop;
52 private boolean placed =
false;
55 private float downRawX, downRawY, startTransX, startTransY;
56 private boolean dragging =
false;
57 private boolean holdDragging =
false;
59 private int cursorTint;
61 private final Handler uiHandler =
new Handler(Looper.getMainLooper());
62 private final Runnable longPress = () ->
64 if (!dragging && !holdDragging)
71 private final RateScroller vScroller =
new RateScroller(
false);
72 private final RateScroller hScroller =
new RateScroller(
true);
75 private final class RateScroller
implements Runnable
77 private final boolean horizontal;
79 private float anchor, current, accum, baseDim;
80 private boolean active;
81 private ValueAnimator animator;
83 RateScroller(
boolean horizontal)
85 this.horizontal = horizontal;
91 v.setOnTouchListener((view, e) -> onTouch(e));
95 private float pos(MotionEvent e)
97 return horizontal ? e.getRawX() : -e.getRawY();
100 private void send(
int notch)
102 if (listener ==
null)
105 listener.onTouchPointerHScroll(notch);
107 listener.onTouchPointerScroll(notch);
110 private boolean onTouch(MotionEvent e)
112 switch (e.getActionMasked())
114 case MotionEvent.ACTION_DOWN:
115 anchor = current = pos(e);
118 uiHandler.post(
this);
119 button.setActivated(
true);
120 button.bringToFront();
123 case MotionEvent.ACTION_MOVE:
126 case MotionEvent.ACTION_UP:
127 case MotionEvent.ACTION_CANCEL:
136 uiHandler.removeCallbacks(
this);
141 button.setActivated(
false);
145 @Override
public void run()
147 float dispDp = (current - anchor) / density;
148 float adisp = Math.abs(dispDp);
149 if (adisp <= SCROLL_DEADZONE_DP)
156 Math.min((adisp - SCROLL_DEADZONE_DP) * SCROLL_NPS_PER_DP, SCROLL_MAX_NPS);
157 accum += nps * SCROLL_TICK_MS / 1000f;
158 int notch = dispDp > 0 ? Mouse.WHEEL_DELTA : -Mouse.WHEEL_DELTA;
159 while (accum >= 1.0f)
165 uiHandler.postDelayed(
this, SCROLL_TICK_MS);
169 private void morph(
boolean expand)
171 float from = horizontal ? button.getWidth() : button.getHeight();
175 expand ? getResources().getDimensionPixelSize(R.dimen.tp_cluster_size) : baseDim;
176 if (animator !=
null)
178 animator = ValueAnimator.ofFloat(from, target);
179 animator.setDuration(140);
180 animator.addUpdateListener(a -> {
181 float val = (float)a.getAnimatedValue();
182 ViewGroup.LayoutParams lp = button.getLayoutParams();
185 lp.width = Math.round(val);
186 button.setTranslationX(-(val - baseDim) / 2.0f);
190 lp.height = Math.round(val);
191 button.setTranslationY(-(val - baseDim) / 2.0f);
193 button.setLayoutParams(lp);
206 this(context, attrs, 0);
211 super(context, attrs, defStyle);
212 initTouchPointer(context);
215 private void initTouchPointer(Context context)
217 density = getResources().getDisplayMetrics().density;
218 touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
219 cursorTint = ContextCompat.getColor(context, R.color.tp_icon);
220 setClipChildren(
false);
222 LayoutInflater.from(context).inflate(R.layout.touch_pointer,
this,
true);
223 cluster = findViewById(R.id.tp_cluster);
224 cursor = findViewById(R.id.tp_cursor);
225 vScroller.attach(findViewById(R.id.tp_scroll));
226 hScroller.attach(findViewById(R.id.tp_hscroll));
228 findViewById(R.id.tp_puck).setOnTouchListener((v, e) -> onPuckTouch(e));
230 findViewById(R.id.tp_close).setOnClickListener(v -> {
231 if (listener !=
null)
232 listener.onTouchPointerClose();
234 findViewById(R.id.tp_rclick).setOnClickListener(v -> {
238 findViewById(R.id.tp_mclick).setOnClickListener(v -> {
242 findViewById(R.id.tp_reset).setOnClickListener(v -> {
243 if (listener !=
null)
244 listener.onTouchPointerResetScrollZoom();
246 findViewById(R.id.tp_keyboard).setOnClickListener(v -> {
247 if (listener !=
null)
248 listener.onTouchPointerToggleKeyboard();
254 this.listener = listener;
257 public int getPointerWidth()
259 return cluster.getWidth() > 0
261 : getResources().getDimensionPixelSize(R.dimen.tp_cluster_size);
264 public int getPointerHeight()
266 return cluster.getHeight() > 0
267 ? cluster.getHeight()
268 : getResources().getDimensionPixelSize(R.dimen.tp_cluster_size);
271 public float[] getPointerPosition()
273 return new float[] { cluster.getX(), cluster.getY() };
277 private int[] hotspot()
279 return new int[] { (int)cluster.getX(), (int)cluster.getY() };
282 private void sendLeft(
boolean down)
285 if (listener !=
null)
286 listener.onTouchPointerLeftClick(h[0], h[1], down);
289 private void sendRight(
boolean down)
292 if (listener !=
null)
293 listener.onTouchPointerRightClick(h[0], h[1], down);
296 private void sendMiddle(
boolean down)
299 if (listener !=
null)
300 listener.onTouchPointerMiddleClick(h[0], h[1], down);
303 private void sendMove()
306 if (listener !=
null)
307 listener.onTouchPointerMove(h[0], h[1]);
310 private void setClusterTranslation(
float tx,
float ty)
312 float maxX = getWidth() - cluster.getWidth();
313 float maxY = getHeight() - cluster.getHeight();
318 if (maxX > 0 && tx > maxX)
320 if (maxY > 0 && ty > maxY)
322 cluster.setTranslationX(tx);
323 cluster.setTranslationY(ty);
326 @Override
protected void onLayout(
boolean changed,
int l,
int t,
int r,
int b)
328 super.onLayout(changed, l, t, r, b);
329 if (!placed && getWidth() > 0 && cluster.getWidth() > 0)
332 setClusterTranslation((getWidth() - cluster.getWidth()) / 2.0f,
333 (getHeight() - cluster.getHeight()) / 2.0f);
337 setClusterTranslation(cluster.getTranslationX(), cluster.getTranslationY());
341 private boolean onPuckTouch(MotionEvent e)
343 switch (e.getActionMasked())
345 case MotionEvent.ACTION_DOWN:
346 downRawX = e.getRawX();
347 downRawY = e.getRawY();
348 startTransX = cluster.getTranslationX();
349 startTransY = cluster.getTranslationY();
351 holdDragging =
false;
352 uiHandler.postDelayed(longPress, LONG_PRESS_MS);
354 case MotionEvent.ACTION_MOVE:
356 float dx = e.getRawX() - downRawX;
357 float dy = e.getRawY() - downRawY;
358 if (!dragging && Math.hypot(dx, dy) > touchSlop)
362 uiHandler.removeCallbacks(longPress);
364 if (dragging || holdDragging)
366 setClusterTranslation(startTransX + dx, startTransY + dy);
371 case MotionEvent.ACTION_UP:
372 uiHandler.removeCallbacks(longPress);
376 holdDragging =
false;
384 if (listener !=
null)
385 listener.onTouchPointerMoveEnd();
387 case MotionEvent.ACTION_CANCEL:
388 uiHandler.removeCallbacks(longPress);
392 holdDragging =
false;
394 if (listener !=
null)
395 listener.onTouchPointerMoveEnd();
403 private void cancelHeldGestures()
406 if (uiHandler ==
null)
408 uiHandler.removeCallbacks(longPress);
413 @Override
protected void onDetachedFromWindow()
415 cancelHeldGestures();
416 super.onDetachedFromWindow();
419 @Override
protected void onVisibilityChanged(View changedView,
int visibility)
421 super.onVisibilityChanged(changedView, visibility);
422 if (visibility != VISIBLE)
423 cancelHeldGestures();
427 public void setRemoteCursor(
int[] pixels,
int width,
int height,
int hotX,
int hotY)
429 ViewGroup.LayoutParams lp = cursor.getLayoutParams();
430 if (pixels ==
null || width <= 0 || height <= 0)
432 cursor.setImageResource(R.drawable.ic_cursor);
433 ImageViewCompat.setImageTintList(cursor, ColorStateList.valueOf(cursorTint));
434 int s = getResources().getDimensionPixelSize(R.dimen.tp_cursor_size);
437 cursor.setLayoutParams(lp);
438 cursor.setTranslationX(0);
439 cursor.setTranslationY(0);
442 Bitmap bmp = Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888);
443 float scale = 40 * density / height;
448 ImageViewCompat.setImageTintList(cursor,
null);
450 BitmapDrawable bd =
new BitmapDrawable(getResources(), bmp);
451 bd.setFilterBitmap(
false);
452 cursor.setImageDrawable(bd);
453 lp.width = Math.round(width * scale);
454 lp.height = Math.round(height * scale);
455 cursor.setLayoutParams(lp);
457 cursor.setTranslationX(-hotX * scale);
458 cursor.setTranslationY(-hotY * scale);
464 void onTouchPointerClose();
466 void onTouchPointerLeftClick(
int x,
int y,
boolean down);
468 void onTouchPointerRightClick(
int x,
int y,
boolean down);
470 void onTouchPointerMiddleClick(
int x,
int y,
boolean down);
472 void onTouchPointerMove(
int x,
int y);
474 void onTouchPointerMoveEnd();
476 void onTouchPointerScroll(
int amount);
478 void onTouchPointerHScroll(
int amount);
480 void onTouchPointerToggleKeyboard();
482 void onTouchPointerResetScrollZoom();