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