• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.screenshot;
18 
19 import android.animation.ValueAnimator;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.Canvas;
23 import android.graphics.Color;
24 import android.graphics.Paint;
25 import android.graphics.Rect;
26 import android.graphics.RectF;
27 import android.os.Bundle;
28 import android.os.Parcel;
29 import android.os.Parcelable;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.util.MathUtils;
33 import android.util.Range;
34 import android.view.KeyEvent;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.accessibility.AccessibilityEvent;
38 import android.view.accessibility.AccessibilityNodeInfo;
39 import android.widget.SeekBar;
40 
41 import androidx.annotation.Nullable;
42 import androidx.core.view.ViewCompat;
43 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
44 import androidx.customview.widget.ExploreByTouchHelper;
45 import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
46 
47 import com.android.internal.graphics.ColorUtils;
48 import com.android.systemui.R;
49 
50 import java.util.List;
51 
52 /**
53  * CropView has top and bottom draggable crop handles, with a scrim to darken the areas being
54  * cropped out.
55  */
56 public class CropView extends View {
57     private static final String TAG = "CropView";
58 
59     public enum CropBoundary {
60         NONE, TOP, BOTTOM, LEFT, RIGHT
61     }
62 
63     private final float mCropTouchMargin;
64     private final Paint mShadePaint;
65     private final Paint mHandlePaint;
66     private final Paint mContainerBackgroundPaint;
67 
68     // Crop rect with each element represented as [0,1] along its proper axis.
69     private RectF mCrop = new RectF(0, 0, 1, 1);
70 
71     private int mExtraTopPadding;
72     private int mExtraBottomPadding;
73     private int mImageWidth;
74 
75     private CropBoundary mCurrentDraggingBoundary = CropBoundary.NONE;
76     private int mActivePointerId;
77     // The starting value of mCurrentDraggingBoundary's crop, used to compute touch deltas.
78     private float mMovementStartValue;
79     private float mStartingY;  // y coordinate of ACTION_DOWN
80     private float mStartingX;
81     // The allowable values for the current boundary being dragged
82     private Range<Float> mMotionRange;
83 
84     // Value [0,1] indicating progress in animateEntrance()
85     private float mEntranceInterpolation = 1f;
86 
87     private CropInteractionListener mCropInteractionListener;
88     private final ExploreByTouchHelper mExploreByTouchHelper;
89 
CropView(Context context, @Nullable AttributeSet attrs)90     public CropView(Context context, @Nullable AttributeSet attrs) {
91         this(context, attrs, 0);
92     }
93 
CropView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)94     public CropView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
95         super(context, attrs, defStyleAttr);
96         TypedArray t = context.getTheme().obtainStyledAttributes(
97                 attrs, R.styleable.CropView, 0, 0);
98         mShadePaint = new Paint();
99         int alpha = t.getInteger(R.styleable.CropView_scrimAlpha, 255);
100         int scrimColor = t.getColor(R.styleable.CropView_scrimColor, Color.TRANSPARENT);
101         mShadePaint.setColor(ColorUtils.setAlphaComponent(scrimColor, alpha));
102         mContainerBackgroundPaint = new Paint();
103         mContainerBackgroundPaint.setColor(t.getColor(R.styleable.CropView_containerBackgroundColor,
104                 Color.TRANSPARENT));
105         mHandlePaint = new Paint();
106         mHandlePaint.setColor(t.getColor(R.styleable.CropView_handleColor, Color.BLACK));
107         mHandlePaint.setStrokeCap(Paint.Cap.ROUND);
108         mHandlePaint.setStrokeWidth(
109                 t.getDimensionPixelSize(R.styleable.CropView_handleThickness, 20));
110         t.recycle();
111         // 48 dp touchable region around each handle.
112         mCropTouchMargin = 24 * getResources().getDisplayMetrics().density;
113 
114         mExploreByTouchHelper = new AccessibilityHelper();
115         ViewCompat.setAccessibilityDelegate(this, mExploreByTouchHelper);
116     }
117 
118     @Override
onSaveInstanceState()119     protected Parcelable onSaveInstanceState() {
120         Parcelable superState = super.onSaveInstanceState();
121 
122         SavedState ss = new SavedState(superState);
123         ss.mCrop = mCrop;
124         return ss;
125     }
126 
127     @Override
onRestoreInstanceState(Parcelable state)128     protected void onRestoreInstanceState(Parcelable state) {
129         SavedState ss = (SavedState) state;
130         super.onRestoreInstanceState(ss.getSuperState());
131 
132         mCrop = ss.mCrop;
133     }
134 
135     @Override
onDraw(Canvas canvas)136     public void onDraw(Canvas canvas) {
137         super.onDraw(canvas);
138         // Top and bottom borders reflect the boundary between the (scrimmed) image and the
139         // opaque container background. This is only meaningful during an entrance transition.
140         float topBorder = MathUtils.lerp(mCrop.top, 0, mEntranceInterpolation);
141         float bottomBorder = MathUtils.lerp(mCrop.bottom, 1, mEntranceInterpolation);
142         drawShade(canvas, 0, topBorder, 1, mCrop.top);
143         drawShade(canvas, 0, mCrop.bottom, 1, bottomBorder);
144         drawShade(canvas, 0, mCrop.top, mCrop.left, mCrop.bottom);
145         drawShade(canvas, mCrop.right, mCrop.top, 1, mCrop.bottom);
146 
147         // Entrance transition expects the crop bounds to be full width, so we only draw container
148         // background on the top and bottom.
149         drawContainerBackground(canvas, 0, 0, 1, topBorder);
150         drawContainerBackground(canvas, 0, bottomBorder, 1, 1);
151 
152         mHandlePaint.setAlpha((int) (mEntranceInterpolation * 255));
153 
154         drawHorizontalHandle(canvas, mCrop.top, /* draw the handle tab up */ true);
155         drawHorizontalHandle(canvas, mCrop.bottom, /* draw the handle tab down */ false);
156         drawVerticalHandle(canvas, mCrop.left, /* left */ true);
157         drawVerticalHandle(canvas, mCrop.right, /* right */ false);
158     }
159 
160     @Override
onTouchEvent(MotionEvent event)161     public boolean onTouchEvent(MotionEvent event) {
162         int topPx = fractionToVerticalPixels(mCrop.top);
163         int bottomPx = fractionToVerticalPixels(mCrop.bottom);
164         switch (event.getActionMasked()) {
165             case MotionEvent.ACTION_DOWN:
166                 mCurrentDraggingBoundary = nearestBoundary(event, topPx, bottomPx,
167                         fractionToHorizontalPixels(mCrop.left),
168                         fractionToHorizontalPixels(mCrop.right));
169                 if (mCurrentDraggingBoundary != CropBoundary.NONE) {
170                     mActivePointerId = event.getPointerId(0);
171                     mStartingY = event.getY();
172                     mStartingX = event.getX();
173                     mMovementStartValue = getBoundaryPosition(mCurrentDraggingBoundary);
174                     updateListener(MotionEvent.ACTION_DOWN, event.getX());
175                     mMotionRange = getAllowedValues(mCurrentDraggingBoundary);
176                 }
177                 return true;
178             case MotionEvent.ACTION_MOVE:
179                 if (mCurrentDraggingBoundary != CropBoundary.NONE) {
180                     int pointerIndex = event.findPointerIndex(mActivePointerId);
181                     if (pointerIndex >= 0) {
182                         // Original pointer still active, do the move.
183                         float deltaPx = isVertical(mCurrentDraggingBoundary)
184                                 ? event.getY(pointerIndex) - mStartingY
185                                 : event.getX(pointerIndex) - mStartingX;
186                         float delta = pixelDistanceToFraction((int) deltaPx,
187                                 mCurrentDraggingBoundary);
188                         setBoundaryPosition(mCurrentDraggingBoundary,
189                                 mMotionRange.clamp(mMovementStartValue + delta));
190                         updateListener(MotionEvent.ACTION_MOVE, event.getX(pointerIndex));
191                         invalidate();
192                     }
193                     return true;
194                 }
195                 break;
196             case MotionEvent.ACTION_POINTER_DOWN:
197                 if (mActivePointerId == event.getPointerId(event.getActionIndex())
198                         && mCurrentDraggingBoundary != CropBoundary.NONE) {
199                     updateListener(MotionEvent.ACTION_DOWN, event.getX(event.getActionIndex()));
200                     return true;
201                 }
202                 break;
203             case MotionEvent.ACTION_POINTER_UP:
204                 if (mActivePointerId == event.getPointerId(event.getActionIndex())
205                         && mCurrentDraggingBoundary != CropBoundary.NONE) {
206                     updateListener(MotionEvent.ACTION_UP, event.getX(event.getActionIndex()));
207                     return true;
208                 }
209                 break;
210             case MotionEvent.ACTION_CANCEL:
211             case MotionEvent.ACTION_UP:
212                 if (mCurrentDraggingBoundary != CropBoundary.NONE
213                         && mActivePointerId == event.getPointerId(mActivePointerId)) {
214                     updateListener(MotionEvent.ACTION_UP, event.getX(0));
215                     return true;
216                 }
217                 break;
218         }
219         return super.onTouchEvent(event);
220     }
221 
222     @Override
dispatchHoverEvent(MotionEvent event)223     public boolean dispatchHoverEvent(MotionEvent event) {
224         return mExploreByTouchHelper.dispatchHoverEvent(event)
225                 || super.dispatchHoverEvent(event);
226     }
227 
228     @Override
dispatchKeyEvent(KeyEvent event)229     public boolean dispatchKeyEvent(KeyEvent event) {
230         return mExploreByTouchHelper.dispatchKeyEvent(event)
231                 || super.dispatchKeyEvent(event);
232     }
233 
234     @Override
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)235     public void onFocusChanged(boolean gainFocus, int direction,
236             Rect previouslyFocusedRect) {
237         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
238         mExploreByTouchHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
239     }
240 
241     /**
242      * Set the given boundary to the given value without animation.
243      */
setBoundaryPosition(CropBoundary boundary, float position)244     public void setBoundaryPosition(CropBoundary boundary, float position) {
245         position = (float) getAllowedValues(boundary).clamp(position);
246         switch (boundary) {
247             case TOP:
248                 mCrop.top = position;
249                 break;
250             case BOTTOM:
251                 mCrop.bottom = position;
252                 break;
253             case LEFT:
254                 mCrop.left = position;
255                 break;
256             case RIGHT:
257                 mCrop.right = position;
258                 break;
259             case NONE:
260                 Log.w(TAG, "No boundary selected");
261                 break;
262         }
263 
264         invalidate();
265     }
266 
getBoundaryPosition(CropBoundary boundary)267     private float getBoundaryPosition(CropBoundary boundary) {
268         switch (boundary) {
269             case TOP:
270                 return mCrop.top;
271             case BOTTOM:
272                 return mCrop.bottom;
273             case LEFT:
274                 return mCrop.left;
275             case RIGHT:
276                 return mCrop.right;
277         }
278         return 0;
279     }
280 
isVertical(CropBoundary boundary)281     private static boolean isVertical(CropBoundary boundary) {
282         return boundary == CropBoundary.TOP || boundary == CropBoundary.BOTTOM;
283     }
284 
285     /**
286      * Animate the given boundary to the given value.
287      */
animateBoundaryTo(CropBoundary boundary, float value)288     public void animateBoundaryTo(CropBoundary boundary, float value) {
289         if (boundary == CropBoundary.NONE) {
290             Log.w(TAG, "No boundary selected for animation");
291             return;
292         }
293         float start = getBoundaryPosition(boundary);
294         ValueAnimator animator = new ValueAnimator();
295         animator.addUpdateListener(animation -> {
296             setBoundaryPosition(boundary,
297                     MathUtils.lerp(start, value, animation.getAnimatedFraction()));
298             invalidate();
299         });
300         animator.setFloatValues(0f, 1f);
301         animator.setDuration(750);
302         animator.setInterpolator(new FastOutSlowInInterpolator());
303         animator.start();
304     }
305 
306     /**
307      * Fade in crop bounds, animate reveal of cropped-out area from current crop bounds.
308      */
animateEntrance()309     public void animateEntrance() {
310         mEntranceInterpolation = 0;
311         ValueAnimator animator = new ValueAnimator();
312         animator.addUpdateListener(animation -> {
313             mEntranceInterpolation = animation.getAnimatedFraction();
314             invalidate();
315         });
316         animator.setFloatValues(0f, 1f);
317         animator.setDuration(750);
318         animator.setInterpolator(new FastOutSlowInInterpolator());
319         animator.start();
320     }
321 
322     /**
323      * Set additional top and bottom padding for the image being cropped (used when the
324      * corresponding ImageView doesn't take the full height).
325      */
setExtraPadding(int top, int bottom)326     public void setExtraPadding(int top, int bottom) {
327         mExtraTopPadding = top;
328         mExtraBottomPadding = bottom;
329         invalidate();
330     }
331 
332     /**
333      * Set the pixel width of the image on the screen (on-screen dimension, not actual bitmap
334      * dimension)
335      */
setImageWidth(int width)336     public void setImageWidth(int width) {
337         mImageWidth = width;
338         invalidate();
339     }
340 
341     /**
342      * @return RectF with values [0,1] representing the position of the boundaries along image axes.
343      */
getCropBoundaries(int imageWidth, int imageHeight)344     public Rect getCropBoundaries(int imageWidth, int imageHeight) {
345         return new Rect((int) (mCrop.left * imageWidth), (int) (mCrop.top * imageHeight),
346                 (int) (mCrop.right * imageWidth), (int) (mCrop.bottom * imageHeight));
347     }
348 
setCropInteractionListener(CropInteractionListener listener)349     public void setCropInteractionListener(CropInteractionListener listener) {
350         mCropInteractionListener = listener;
351     }
352 
getAllowedValues(CropBoundary boundary)353     private Range getAllowedValues(CropBoundary boundary) {
354         switch (boundary) {
355             case TOP:
356                 return new Range<>(0f,
357                         mCrop.bottom - pixelDistanceToFraction(mCropTouchMargin,
358                                 CropBoundary.BOTTOM));
359             case BOTTOM:
360                 return new Range<>(
361                         mCrop.top + pixelDistanceToFraction(mCropTouchMargin,
362                                 CropBoundary.TOP), 1f);
363             case LEFT:
364                 return new Range<>(0f,
365                         mCrop.right - pixelDistanceToFraction(mCropTouchMargin,
366                                 CropBoundary.RIGHT));
367             case RIGHT:
368                 return new Range<>(
369                         mCrop.left + pixelDistanceToFraction(mCropTouchMargin,
370                                 CropBoundary.LEFT), 1f);
371         }
372         return null;
373     }
374 
375     /**
376      * @param action either ACTION_DOWN, ACTION_UP or ACTION_MOVE.
377      * @param x coordinate of the relevant pointer.
378      */
updateListener(int action, float x)379     private void updateListener(int action, float x) {
380         if (mCropInteractionListener != null && isVertical(mCurrentDraggingBoundary)) {
381             float boundaryPosition = getBoundaryPosition(mCurrentDraggingBoundary);
382             switch (action) {
383                 case MotionEvent.ACTION_DOWN:
384                     mCropInteractionListener.onCropDragStarted(mCurrentDraggingBoundary,
385                             boundaryPosition, fractionToVerticalPixels(boundaryPosition),
386                             (mCrop.left + mCrop.right) / 2, x);
387                     break;
388                 case MotionEvent.ACTION_MOVE:
389                     mCropInteractionListener.onCropDragMoved(mCurrentDraggingBoundary,
390                             boundaryPosition, fractionToVerticalPixels(boundaryPosition),
391                             (mCrop.left + mCrop.right) / 2, x);
392                     break;
393                 case MotionEvent.ACTION_UP:
394                     mCropInteractionListener.onCropDragComplete();
395                     break;
396 
397             }
398         }
399     }
400 
401     /**
402      * Draw a shade to the given canvas with the given [0,1] fractional image bounds.
403      */
drawShade(Canvas canvas, float left, float top, float right, float bottom)404     private void drawShade(Canvas canvas, float left, float top, float right, float bottom) {
405         canvas.drawRect(fractionToHorizontalPixels(left), fractionToVerticalPixels(top),
406                 fractionToHorizontalPixels(right),
407                 fractionToVerticalPixels(bottom), mShadePaint);
408     }
409 
drawContainerBackground(Canvas canvas, float left, float top, float right, float bottom)410     private void drawContainerBackground(Canvas canvas, float left, float top, float right,
411             float bottom) {
412         canvas.drawRect(fractionToHorizontalPixels(left), fractionToVerticalPixels(top),
413                 fractionToHorizontalPixels(right),
414                 fractionToVerticalPixels(bottom), mContainerBackgroundPaint);
415     }
416 
drawHorizontalHandle(Canvas canvas, float frac, boolean handleTabUp)417     private void drawHorizontalHandle(Canvas canvas, float frac, boolean handleTabUp) {
418         int y = fractionToVerticalPixels(frac);
419         canvas.drawLine(fractionToHorizontalPixels(mCrop.left), y,
420                 fractionToHorizontalPixels(mCrop.right), y, mHandlePaint);
421         float radius = 8 * getResources().getDisplayMetrics().density;
422         int x = (fractionToHorizontalPixels(mCrop.left) + fractionToHorizontalPixels(mCrop.right))
423                 / 2;
424         canvas.drawArc(x - radius, y - radius, x + radius, y + radius, handleTabUp ? 180 : 0, 180,
425                 true, mHandlePaint);
426     }
427 
drawVerticalHandle(Canvas canvas, float frac, boolean handleTabLeft)428     private void drawVerticalHandle(Canvas canvas, float frac, boolean handleTabLeft) {
429         int x = fractionToHorizontalPixels(frac);
430         canvas.drawLine(x, fractionToVerticalPixels(mCrop.top), x,
431                 fractionToVerticalPixels(mCrop.bottom), mHandlePaint);
432         float radius = 8 * getResources().getDisplayMetrics().density;
433         int y = (fractionToVerticalPixels(getBoundaryPosition(CropBoundary.TOP))
434                 + fractionToVerticalPixels(
435                 getBoundaryPosition(CropBoundary.BOTTOM))) / 2;
436         canvas.drawArc(x - radius, y - radius, x + radius, y + radius, handleTabLeft ? 90 : 270,
437                 180,
438                 true, mHandlePaint);
439     }
440 
441     /**
442      * Convert the given fraction position to pixel position within the View.
443      */
fractionToVerticalPixels(float frac)444     private int fractionToVerticalPixels(float frac) {
445         return (int) (mExtraTopPadding + frac * getImageHeight());
446     }
447 
fractionToHorizontalPixels(float frac)448     private int fractionToHorizontalPixels(float frac) {
449         return (int) ((getWidth() - mImageWidth) / 2 + frac * mImageWidth);
450     }
451 
getImageHeight()452     private int getImageHeight() {
453         return getHeight() - mExtraTopPadding - mExtraBottomPadding;
454     }
455 
456     /**
457      * Convert the given pixel distance to fraction of the image.
458      */
pixelDistanceToFraction(float px, CropBoundary boundary)459     private float pixelDistanceToFraction(float px, CropBoundary boundary) {
460         if (isVertical(boundary)) {
461             return px / getImageHeight();
462         } else {
463             return px / mImageWidth;
464         }
465     }
466 
nearestBoundary(MotionEvent event, int topPx, int bottomPx, int leftPx, int rightPx)467     private CropBoundary nearestBoundary(MotionEvent event, int topPx, int bottomPx, int leftPx,
468             int rightPx) {
469         if (Math.abs(event.getY() - topPx) < mCropTouchMargin) {
470             return CropBoundary.TOP;
471         }
472         if (Math.abs(event.getY() - bottomPx) < mCropTouchMargin) {
473             return CropBoundary.BOTTOM;
474         }
475         if (event.getY() > topPx || event.getY() < bottomPx) {
476             if (Math.abs(event.getX() - leftPx) < mCropTouchMargin) {
477                 return CropBoundary.LEFT;
478             }
479             if (Math.abs(event.getX() - rightPx) < mCropTouchMargin) {
480                 return CropBoundary.RIGHT;
481             }
482         }
483         return CropBoundary.NONE;
484     }
485 
486     private class AccessibilityHelper extends ExploreByTouchHelper {
487 
488         private static final int TOP_HANDLE_ID = 1;
489         private static final int BOTTOM_HANDLE_ID = 2;
490         private static final int LEFT_HANDLE_ID = 3;
491         private static final int RIGHT_HANDLE_ID = 4;
492 
AccessibilityHelper()493         AccessibilityHelper() {
494             super(CropView.this);
495         }
496 
497         @Override
getVirtualViewAt(float x, float y)498         protected int getVirtualViewAt(float x, float y) {
499             if (Math.abs(y - fractionToVerticalPixels(mCrop.top)) < mCropTouchMargin) {
500                 return TOP_HANDLE_ID;
501             }
502             if (Math.abs(y - fractionToVerticalPixels(mCrop.bottom)) < mCropTouchMargin) {
503                 return BOTTOM_HANDLE_ID;
504             }
505             if (y > fractionToVerticalPixels(mCrop.top)
506                     && y < fractionToVerticalPixels(mCrop.bottom)) {
507                 if (Math.abs(x - fractionToHorizontalPixels(mCrop.left)) < mCropTouchMargin) {
508                     return LEFT_HANDLE_ID;
509                 }
510                 if (Math.abs(x - fractionToHorizontalPixels(mCrop.right)) < mCropTouchMargin) {
511                     return RIGHT_HANDLE_ID;
512                 }
513             }
514 
515             return ExploreByTouchHelper.HOST_ID;
516         }
517 
518         @Override
getVisibleVirtualViews(List<Integer> virtualViewIds)519         protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
520             // Add views in traversal order
521             virtualViewIds.add(TOP_HANDLE_ID);
522             virtualViewIds.add(LEFT_HANDLE_ID);
523             virtualViewIds.add(RIGHT_HANDLE_ID);
524             virtualViewIds.add(BOTTOM_HANDLE_ID);
525         }
526 
527         @Override
onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event)528         protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
529             CropBoundary boundary = viewIdToBoundary(virtualViewId);
530             event.setContentDescription(getBoundaryContentDescription(boundary));
531         }
532 
533         @Override
onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node)534         protected void onPopulateNodeForVirtualView(int virtualViewId,
535                 AccessibilityNodeInfoCompat node) {
536             CropBoundary boundary = viewIdToBoundary(virtualViewId);
537             node.setContentDescription(getBoundaryContentDescription(boundary));
538             setNodePosition(getNodeRect(boundary), node);
539 
540             // Intentionally set the class name to SeekBar so that TalkBack uses volume control to
541             // scroll.
542             node.setClassName(SeekBar.class.getName());
543             node.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
544             node.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
545         }
546 
547         @Override
onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments)548         protected boolean onPerformActionForVirtualView(
549                 int virtualViewId, int action, Bundle arguments) {
550             if (action != AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
551                     && action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
552                 return false;
553             }
554             CropBoundary boundary = viewIdToBoundary(virtualViewId);
555             float delta = pixelDistanceToFraction(mCropTouchMargin, boundary);
556             if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
557                 delta = -delta;
558             }
559             setBoundaryPosition(boundary, delta + getBoundaryPosition(boundary));
560             invalidateVirtualView(virtualViewId);
561             sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
562             return true;
563         }
564 
getBoundaryContentDescription(CropBoundary boundary)565         private CharSequence getBoundaryContentDescription(CropBoundary boundary) {
566             int template;
567             switch (boundary) {
568                 case TOP:
569                     template = R.string.screenshot_top_boundary_pct;
570                     break;
571                 case BOTTOM:
572                     template = R.string.screenshot_bottom_boundary_pct;
573                     break;
574                 case LEFT:
575                     template = R.string.screenshot_left_boundary_pct;
576                     break;
577                 case RIGHT:
578                     template = R.string.screenshot_right_boundary_pct;
579                     break;
580                 default:
581                     return "";
582             }
583 
584             return getResources().getString(template,
585                     Math.round(getBoundaryPosition(boundary) * 100));
586         }
587 
viewIdToBoundary(int viewId)588         private CropBoundary viewIdToBoundary(int viewId) {
589             switch (viewId) {
590                 case TOP_HANDLE_ID:
591                     return CropBoundary.TOP;
592                 case BOTTOM_HANDLE_ID:
593                     return CropBoundary.BOTTOM;
594                 case LEFT_HANDLE_ID:
595                     return CropBoundary.LEFT;
596                 case RIGHT_HANDLE_ID:
597                     return CropBoundary.RIGHT;
598             }
599             return CropBoundary.NONE;
600         }
601 
getNodeRect(CropBoundary boundary)602         private Rect getNodeRect(CropBoundary boundary) {
603             Rect rect;
604             if (isVertical(boundary)) {
605                 int pixels = fractionToVerticalPixels(getBoundaryPosition(boundary));
606                 rect = new Rect(0, (int) (pixels - mCropTouchMargin),
607                         getWidth(), (int) (pixels + mCropTouchMargin));
608                 // Top boundary can sometimes go beyond the view, shift it down to compensate so
609                 // the area is big enough.
610                 if (rect.top < 0) {
611                     rect.offset(0, -rect.top);
612                 }
613             } else {
614                 int pixels = fractionToHorizontalPixels(getBoundaryPosition(boundary));
615                 rect = new Rect((int) (pixels - mCropTouchMargin),
616                         (int) (fractionToVerticalPixels(mCrop.top) + mCropTouchMargin),
617                         (int) (pixels + mCropTouchMargin),
618                         (int) (fractionToVerticalPixels(mCrop.bottom) - mCropTouchMargin));
619             }
620             return rect;
621         }
622 
setNodePosition(Rect rect, AccessibilityNodeInfoCompat node)623         private void setNodePosition(Rect rect, AccessibilityNodeInfoCompat node) {
624             node.setBoundsInParent(rect);
625             int[] pos = new int[2];
626             getLocationOnScreen(pos);
627             rect.offset(pos[0], pos[1]);
628             node.setBoundsInScreen(rect);
629         }
630     }
631 
632     /**
633      * Listen for crop motion events and state.
634      */
635     public interface CropInteractionListener {
onCropDragStarted(CropBoundary boundary, float boundaryPosition, int boundaryPositionPx, float horizontalCenter, float x)636         void onCropDragStarted(CropBoundary boundary, float boundaryPosition,
637                 int boundaryPositionPx, float horizontalCenter, float x);
onCropDragMoved(CropBoundary boundary, float boundaryPosition, int boundaryPositionPx, float horizontalCenter, float x)638         void onCropDragMoved(CropBoundary boundary, float boundaryPosition,
639                 int boundaryPositionPx, float horizontalCenter, float x);
onCropDragComplete()640         void onCropDragComplete();
641     }
642 
643     static class SavedState extends BaseSavedState {
644         RectF mCrop;
645 
646         /**
647          * Constructor called from {@link CropView#onSaveInstanceState()}
648          */
SavedState(Parcelable superState)649         SavedState(Parcelable superState) {
650             super(superState);
651         }
652 
653         /**
654          * Constructor called from {@link #CREATOR}
655          */
SavedState(Parcel in)656         private SavedState(Parcel in) {
657             super(in);
658             mCrop = in.readParcelable(ClassLoader.getSystemClassLoader());
659         }
660 
661         @Override
writeToParcel(Parcel out, int flags)662         public void writeToParcel(Parcel out, int flags) {
663             super.writeToParcel(out, flags);
664             out.writeParcelable(mCrop, 0);
665         }
666 
667         public static final Parcelable.Creator<SavedState> CREATOR
668                 = new Parcelable.Creator<SavedState>() {
669             public SavedState createFromParcel(Parcel in) {
670                 return new SavedState(in);
671             }
672 
673             public SavedState[] newArray(int size) {
674                 return new SavedState[size];
675             }
676         };
677     }
678 }
679