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
This commit is contained in:
Matt Casey
2021-04-07 12:21:21 -04:00
parent 54c39c71a9
commit 8ccd7eb538
2 changed files with 161 additions and 64 deletions

View File

@@ -245,10 +245,14 @@
<string name="screenshot_dismiss_description">Dismiss screenshot</string>
<!-- Content description indicating that the view is a preview of the screenshot that was just taken [CHAR LIMIT=NONE] -->
<string name="screenshot_preview_description">Screenshot preview</string>
<!-- Content description for the top boundary of the screenshot being cropped [CHAR LIMIT=NONE] -->
<string name="screenshot_top_boundary">Top boundary</string>
<!-- Content description for the bottom boundary of the screenshot being cropped [CHAR LIMIT=NONE] -->
<string name="screenshot_bottom_boundary">Bottom boundary</string>
<!-- Content description for the top boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
<string name="screenshot_top_boundary_pct">Top boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
<!-- Content description for the bottom boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
<string name="screenshot_bottom_boundary_pct">Bottom boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
<!-- Content description for the left boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
<string name="screenshot_left_boundary_pct">Left boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
<!-- Content description for the right boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
<string name="screenshot_right_boundary_pct">Right boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
<!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
<string name="screenrecord_name">Screen Recorder</string>

View File

@@ -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<Float> 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<Integer> 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);