From 8ccd7eb5388272d817f65c248619ea94413948dc Mon Sep 17 00:00:00 2001 From: Matt Casey Date: Wed, 7 Apr 2021 12:21:21 -0400 Subject: [PATCH] TalkBack and switch access support for CropView - Switch to androidx ExploreByTouchHelper, add View pass-through calls. - Add virtual view IDs for left/right bounds as well. - Make content description describe the position (as a %) - Allow volume buttons to move the current boundary along its axis (matching pattern used in Photos) - Remove unused click action for boundaries (was breaking switch access) Bug: 184018281 Bug: 184012364 Bug: 180062481 Bug: 184105678 Test: Enable talkback, move crop bounds with volume keys, same with switch access Change-Id: I06bab02a37620f19e15cddcfc79ae858a1614e92 --- packages/SystemUI/res/values/strings.xml | 12 +- .../android/systemui/screenshot/CropView.java | 213 +++++++++++++----- 2 files changed, 161 insertions(+), 64 deletions(-) diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index fba62691c951b..bed7442d576e5 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -245,10 +245,14 @@ Dismiss screenshot Screenshot preview - - Top boundary - - Bottom boundary + + Top boundary %1$d percent + + Bottom boundary %1$d percent + + Left boundary %1$d percent + + Right boundary %1$d percent Screen Recorder diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java index 78ee896075703..bbce7b09e9b67 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java @@ -28,21 +28,26 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; -import android.util.IntArray; import android.util.Log; import android.util.MathUtils; import android.util.Range; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.SeekBar; import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.customview.widget.ExploreByTouchHelper; import androidx.interpolator.view.animation.FastOutSlowInInterpolator; -import com.android.internal.widget.ExploreByTouchHelper; import com.android.systemui.R; +import java.util.List; + /** * CropView has top and bottom draggable crop handles, with a scrim to darken the areas being * cropped out. @@ -74,6 +79,7 @@ public class CropView extends View { private Range mMotionRange; private CropInteractionListener mCropInteractionListener; + private final ExploreByTouchHelper mExploreByTouchHelper; public CropView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); @@ -94,7 +100,8 @@ public class CropView extends View { // 48 dp touchable region around each handle. mCropTouchMargin = 24 * getResources().getDisplayMetrics().density; - setAccessibilityDelegate(new AccessibilityHelper()); + mExploreByTouchHelper = new AccessibilityHelper(); + ViewCompat.setAccessibilityDelegate(this, mExploreByTouchHelper); } @Override @@ -141,28 +148,7 @@ public class CropView extends View { mStartingX = event.getX(); mMovementStartValue = getBoundaryPosition(mCurrentDraggingBoundary); updateListener(event); - switch (mCurrentDraggingBoundary) { - case TOP: - mMotionRange = new Range<>(0f, - mCrop.bottom - pixelDistanceToFraction(mCropTouchMargin, - CropBoundary.BOTTOM)); - break; - case BOTTOM: - mMotionRange = new Range<>( - mCrop.top + pixelDistanceToFraction(mCropTouchMargin, - CropBoundary.TOP), 1f); - break; - case LEFT: - mMotionRange = new Range<>(0f, - mCrop.right - pixelDistanceToFraction(mCropTouchMargin, - CropBoundary.RIGHT)); - break; - case RIGHT: - mMotionRange = new Range<>( - mCrop.left + pixelDistanceToFraction(mCropTouchMargin, - CropBoundary.LEFT), 1f); - break; - } + mMotionRange = getAllowedValues(mCurrentDraggingBoundary); } return true; case MotionEvent.ACTION_MOVE: @@ -185,10 +171,30 @@ public class CropView extends View { return super.onTouchEvent(event); } + @Override + public boolean dispatchHoverEvent(MotionEvent event) { + return mExploreByTouchHelper.dispatchHoverEvent(event) + || super.dispatchHoverEvent(event); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return mExploreByTouchHelper.dispatchKeyEvent(event) + || super.dispatchKeyEvent(event); + } + + @Override + public void onFocusChanged(boolean gainFocus, int direction, + Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + mExploreByTouchHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + } + /** * Set the given boundary to the given value without animation. */ public void setBoundaryPosition(CropBoundary boundary, float position) { + position = (float) getAllowedValues(boundary).clamp(position); switch (boundary) { case TOP: mCrop.top = position; @@ -280,6 +286,28 @@ public class CropView extends View { mCropInteractionListener = listener; } + private Range getAllowedValues(CropBoundary boundary) { + switch (boundary) { + case TOP: + return new Range<>(0f, + mCrop.bottom - pixelDistanceToFraction(mCropTouchMargin, + CropBoundary.BOTTOM)); + case BOTTOM: + return new Range<>( + mCrop.top + pixelDistanceToFraction(mCropTouchMargin, + CropBoundary.TOP), 1f); + case LEFT: + return new Range<>(0f, + mCrop.right - pixelDistanceToFraction(mCropTouchMargin, + CropBoundary.RIGHT)); + case RIGHT: + return new Range<>( + mCrop.left + pixelDistanceToFraction(mCropTouchMargin, + CropBoundary.LEFT), 1f); + } + return null; + } + private void updateListener(MotionEvent event) { if (mCropInteractionListener != null && (isVertical(mCurrentDraggingBoundary))) { float boundaryPosition = getBoundaryPosition(mCurrentDraggingBoundary); @@ -371,6 +399,8 @@ public class CropView extends View { private static final int TOP_HANDLE_ID = 1; private static final int BOTTOM_HANDLE_ID = 2; + private static final int LEFT_HANDLE_ID = 3; + private static final int RIGHT_HANDLE_ID = 4; AccessibilityHelper() { super(CropView.this); @@ -384,62 +414,125 @@ public class CropView extends View { if (Math.abs(y - fractionToVerticalPixels(mCrop.bottom)) < mCropTouchMargin) { return BOTTOM_HANDLE_ID; } - return ExploreByTouchHelper.INVALID_ID; + if (y > fractionToVerticalPixels(mCrop.top) + && y < fractionToVerticalPixels(mCrop.bottom)) { + if (Math.abs(x - fractionToHorizontalPixels(mCrop.left)) < mCropTouchMargin) { + return LEFT_HANDLE_ID; + } + if (Math.abs(x - fractionToHorizontalPixels(mCrop.right)) < mCropTouchMargin) { + return RIGHT_HANDLE_ID; + } + } + + return ExploreByTouchHelper.HOST_ID; } @Override - protected void getVisibleVirtualViews(IntArray virtualViewIds) { + protected void getVisibleVirtualViews(List virtualViewIds) { + // Add views in traversal order virtualViewIds.add(TOP_HANDLE_ID); + virtualViewIds.add(LEFT_HANDLE_ID); + virtualViewIds.add(RIGHT_HANDLE_ID); virtualViewIds.add(BOTTOM_HANDLE_ID); } @Override protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { - switch (virtualViewId) { - case TOP_HANDLE_ID: - event.setContentDescription( - getResources().getString(R.string.screenshot_top_boundary)); - break; - case BOTTOM_HANDLE_ID: - event.setContentDescription( - getResources().getString(R.string.screenshot_bottom_boundary)); - break; - } + CropBoundary boundary = viewIdToBoundary(virtualViewId); + event.setContentDescription(getBoundaryContentDescription(boundary)); } @Override protected void onPopulateNodeForVirtualView(int virtualViewId, - AccessibilityNodeInfo node) { - switch (virtualViewId) { - case TOP_HANDLE_ID: - node.setContentDescription( - getResources().getString(R.string.screenshot_top_boundary)); - setNodePositions(mCrop.top, node); - break; - case BOTTOM_HANDLE_ID: - node.setContentDescription( - getResources().getString(R.string.screenshot_bottom_boundary)); - setNodePositions(mCrop.bottom, node); - break; - } + AccessibilityNodeInfoCompat node) { + CropBoundary boundary = viewIdToBoundary(virtualViewId); + node.setContentDescription(getBoundaryContentDescription(boundary)); + setNodePosition(getNodeRect(boundary), node); - // TODO: need to figure out the full set of actions to support here. - node.addAction( - AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); - node.setClickable(true); - node.setFocusable(true); + // Intentionally set the class name to SeekBar so that TalkBack uses volume control to + // scroll. + node.setClassName(SeekBar.class.getName()); + node.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + node.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); } @Override protected boolean onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments) { - return false; + if (action != AccessibilityNodeInfo.ACTION_SCROLL_FORWARD + && action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + return false; + } + CropBoundary boundary = viewIdToBoundary(virtualViewId); + float delta = pixelDistanceToFraction(mCropTouchMargin, boundary); + if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { + delta = -delta; + } + setBoundaryPosition(boundary, delta + getBoundaryPosition(boundary)); + invalidateVirtualView(virtualViewId); + sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED); + return true; } - private void setNodePositions(float fraction, AccessibilityNodeInfo node) { - int pixels = fractionToVerticalPixels(fraction); - Rect rect = new Rect(0, (int) (pixels - mCropTouchMargin), - getWidth(), (int) (pixels + mCropTouchMargin)); + private CharSequence getBoundaryContentDescription(CropBoundary boundary) { + int template; + switch (boundary) { + case TOP: + template = R.string.screenshot_top_boundary_pct; + break; + case BOTTOM: + template = R.string.screenshot_bottom_boundary_pct; + break; + case LEFT: + template = R.string.screenshot_left_boundary_pct; + break; + case RIGHT: + template = R.string.screenshot_right_boundary_pct; + break; + default: + return ""; + } + + return getResources().getString(template, + Math.round(getBoundaryPosition(boundary) * 100)); + } + + private CropBoundary viewIdToBoundary(int viewId) { + switch (viewId) { + case TOP_HANDLE_ID: + return CropBoundary.TOP; + case BOTTOM_HANDLE_ID: + return CropBoundary.BOTTOM; + case LEFT_HANDLE_ID: + return CropBoundary.LEFT; + case RIGHT_HANDLE_ID: + return CropBoundary.RIGHT; + } + return CropBoundary.NONE; + } + + private Rect getNodeRect(CropBoundary boundary) { + Rect rect; + if (isVertical(boundary)) { + int pixels = fractionToVerticalPixels(getBoundaryPosition(boundary)); + rect = new Rect(0, (int) (pixels - mCropTouchMargin), + getWidth(), (int) (pixels + mCropTouchMargin)); + // Top boundary can sometimes go beyond the view, shift it down to compensate so + // the area is big enough. + if (rect.top < 0) { + rect.offset(0, -rect.top); + } + } else { + int pixels = fractionToHorizontalPixels(getBoundaryPosition(boundary)); + rect = new Rect((int) (pixels - mCropTouchMargin), + (int) (fractionToVerticalPixels(mCrop.top) + mCropTouchMargin), + (int) (pixels + mCropTouchMargin), + (int) (fractionToVerticalPixels(mCrop.bottom) - mCropTouchMargin)); + } + return rect; + } + + private void setNodePosition(Rect rect, AccessibilityNodeInfoCompat node) { node.setBoundsInParent(rect); int[] pos = new int[2]; getLocationOnScreen(pos);