From 0ca415c8ee30f0b28d919c4d574bcdf5b03b9573 Mon Sep 17 00:00:00 2001 From: dronov-dmitry Date: Mon, 17 Aug 2026 11:22:11 +0200 Subject: [PATCH] Add two-finger pan to scroll text in any direction --- .../textpad/activities/EditorActivity.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/app/src/main/java/com/maxistar/textpad/activities/EditorActivity.java b/app/src/main/java/com/maxistar/textpad/activities/EditorActivity.java index dc2795b..5b37a93 100644 --- a/app/src/main/java/com/maxistar/textpad/activities/EditorActivity.java +++ b/app/src/main/java/com/maxistar/textpad/activities/EditorActivity.java @@ -38,10 +38,12 @@ import android.print.PrintManager; import android.text.Editable; import android.text.InputType; +import android.text.Layout; import android.text.Spanned; import android.text.TextWatcher; import android.text.style.BackgroundColorSpan; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.Menu; import android.view.MenuItem; import android.view.WindowInsets; @@ -214,6 +216,7 @@ public void onCreate(Bundle savedInstanceState) { } mText = this.findViewById(R.id.editText1); mText.setBackgroundResource(android.R.color.transparent); + mText.setOnTouchListener(new TwoFingerPanTouchListener()); if (!settingsService.isAutoWrapping()) { disableEditorAutowrapping(); } @@ -1960,6 +1963,105 @@ protected void showToast(String toast_str) { toast.show(); } + // TwoFingerPanTouchListener + private class TwoFingerPanTouchListener implements View.OnTouchListener { + + private boolean panningActive = false; + private float lastFocalX; + private float lastFocalY; + + @Override + public boolean onTouch(View view, MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + panningActive = false; + return false; + case MotionEvent.ACTION_POINTER_DOWN: + if (event.getPointerCount() == 2) { + panningActive = true; + view.getParent().requestDisallowInterceptTouchEvent(true); + lastFocalX = focalX(event); + lastFocalY = focalY(event); + } + return panningActive; + case MotionEvent.ACTION_MOVE: + if (!panningActive) { + return false; + } + float focalX = focalX(event); + float focalY = focalY(event); + panContent( + Math.round(focalX - lastFocalX), + Math.round(focalY - lastFocalY) + ); + lastFocalX = focalX; + lastFocalY = focalY; + return true; + case MotionEvent.ACTION_POINTER_UP: + if (panningActive) { + view.getParent().requestDisallowInterceptTouchEvent(false); + } + return panningActive; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + boolean wasPanning = panningActive; + panningActive = false; + view.getParent().requestDisallowInterceptTouchEvent(false); + return wasPanning; + default: + return panningActive; + } + } + + private float focalX(MotionEvent event) { + float sum = 0; + for (int i = 0; i < event.getPointerCount(); i++) { + sum += event.getX(i); + } + return sum / event.getPointerCount(); + } + + private float focalY(MotionEvent event) { + float sum = 0; + for (int i = 0; i < event.getPointerCount(); i++) { + sum += event.getY(i); + } + return sum / event.getPointerCount(); + } + } + + private void panContent(int deltaX, int deltaY) { + Layout layout = mText.getLayout(); + int maxX = layout == null ? 0 : + layout.getWidth() + mText.getTotalPaddingLeft() + mText.getTotalPaddingRight() + - mText.getWidth(); + int newX = clampScroll(mText.getScrollX() - deltaX, maxX); + + if (simpleScrolling()) { + int maxY = layout == null ? 0 : + layout.getHeight() + mText.getTotalPaddingTop() + mText.getTotalPaddingBottom() + - mText.getHeight(); + int newY = clampScroll(mText.getScrollY() - deltaY, maxY); + mText.scrollTo(newX, newY); + return; + } + + if (scrollView != null) { + scrollView.scrollBy(0, -deltaY); + } + mText.scrollTo(newX, mText.getScrollY()); + } + + private int clampScroll(int value, int max) { + if (value < 0) { + return 0; + } + if (max > 0 && value > max) { + return max; + } + return value; + } + // QueryTextListener private class QueryTextListener implements SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener {