1 /* 2 * Copyright (C) 2008 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.launcher3.dragndrop; 18 19 import static com.android.launcher3.Utilities.getBadge; 20 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.FloatArrayEvaluator; 25 import android.animation.ValueAnimator; 26 import android.animation.ValueAnimator.AnimatorUpdateListener; 27 import android.annotation.TargetApi; 28 import android.graphics.Bitmap; 29 import android.graphics.Canvas; 30 import android.graphics.Color; 31 import android.graphics.ColorMatrix; 32 import android.graphics.ColorMatrixColorFilter; 33 import android.graphics.Paint; 34 import android.graphics.Path; 35 import android.graphics.Point; 36 import android.graphics.Rect; 37 import android.graphics.drawable.AdaptiveIconDrawable; 38 import android.graphics.drawable.ColorDrawable; 39 import android.graphics.drawable.Drawable; 40 import android.os.Build; 41 import android.os.Handler; 42 import android.os.Looper; 43 import android.view.View; 44 45 import androidx.dynamicanimation.animation.FloatPropertyCompat; 46 import androidx.dynamicanimation.animation.SpringAnimation; 47 import androidx.dynamicanimation.animation.SpringForce; 48 49 import com.android.launcher3.FastBitmapDrawable; 50 import com.android.launcher3.FirstFrameAnimatorHelper; 51 import com.android.launcher3.ItemInfo; 52 import com.android.launcher3.Launcher; 53 import com.android.launcher3.LauncherSettings; 54 import com.android.launcher3.LauncherState; 55 import com.android.launcher3.LauncherStateManager; 56 import com.android.launcher3.R; 57 import com.android.launcher3.Utilities; 58 import com.android.launcher3.anim.Interpolators; 59 import com.android.launcher3.icons.LauncherIcons; 60 import com.android.launcher3.util.Themes; 61 import com.android.launcher3.util.Thunk; 62 63 import java.util.Arrays; 64 65 public class DragView extends View implements LauncherStateManager.StateListener { 66 private static final ColorMatrix sTempMatrix1 = new ColorMatrix(); 67 private static final ColorMatrix sTempMatrix2 = new ColorMatrix(); 68 69 public static final int COLOR_CHANGE_DURATION = 120; 70 public static final int VIEW_ZOOM_DURATION = 150; 71 72 private boolean mDrawBitmap = true; 73 private Bitmap mBitmap; 74 private Bitmap mCrossFadeBitmap; 75 @Thunk Paint mPaint; 76 private final int mBlurSizeOutline; 77 private final int mRegistrationX; 78 private final int mRegistrationY; 79 private final float mInitialScale; 80 private final float mScaleOnDrop; 81 private final int[] mTempLoc = new int[2]; 82 83 private Point mDragVisualizeOffset = null; 84 private Rect mDragRegion = null; 85 private final Launcher mLauncher; 86 private final DragLayer mDragLayer; 87 @Thunk final DragController mDragController; 88 final FirstFrameAnimatorHelper mFirstFrameAnimatorHelper; 89 private boolean mHasDrawn = false; 90 @Thunk float mCrossFadeProgress = 0f; 91 private boolean mAnimationCancelled = false; 92 93 ValueAnimator mAnim; 94 // The intrinsic icon scale factor is the scale factor for a drag icon over the workspace 95 // size. This is ignored for non-icons. 96 private float mIntrinsicIconScale = 1f; 97 98 @Thunk float[] mCurrentFilter; 99 private ValueAnimator mFilterAnimator; 100 101 private int mLastTouchX; 102 private int mLastTouchY; 103 private int mAnimatedShiftX; 104 private int mAnimatedShiftY; 105 106 // Below variable only needed IF FeatureFlags.LAUNCHER3_SPRING_ICONS is {@code true} 107 private Drawable mBgSpringDrawable, mFgSpringDrawable; 108 private SpringFloatValue mTranslateX, mTranslateY; 109 private Path mScaledMaskPath; 110 private Drawable mBadge; 111 private ColorMatrixColorFilter mBaseFilter; 112 113 /** 114 * Construct the drag view. 115 * <p> 116 * The registration point is the point inside our view that the touch events should 117 * be centered upon. 118 * @param launcher The Launcher instance 119 * @param bitmap The view that we're dragging around. We scale it up when we draw it. 120 * @param registrationX The x coordinate of the registration point. 121 * @param registrationY The y coordinate of the registration point. 122 */ DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, final float initialScale, final float scaleOnDrop, final float finalScaleDps)123 public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, 124 final float initialScale, final float scaleOnDrop, final float finalScaleDps) { 125 super(launcher); 126 mLauncher = launcher; 127 mDragLayer = launcher.getDragLayer(); 128 mDragController = launcher.getDragController(); 129 mFirstFrameAnimatorHelper = new FirstFrameAnimatorHelper(this); 130 131 final float scale = (bitmap.getWidth() + finalScaleDps) / bitmap.getWidth(); 132 133 // Set the initial scale to avoid any jumps 134 setScaleX(initialScale); 135 setScaleY(initialScale); 136 137 // Animate the view into the correct position 138 mAnim = ValueAnimator.ofFloat(0f, 1f); 139 mAnim.setDuration(VIEW_ZOOM_DURATION); 140 mAnim.addUpdateListener(animation -> { 141 final float value = (Float) animation.getAnimatedValue(); 142 setScaleX(initialScale + (value * (scale - initialScale))); 143 setScaleY(initialScale + (value * (scale - initialScale))); 144 if (!isAttachedToWindow()) { 145 animation.cancel(); 146 } 147 }); 148 149 mAnim.addListener(new AnimatorListenerAdapter() { 150 @Override 151 public void onAnimationEnd(Animator animation) { 152 if (!mAnimationCancelled) { 153 mDragController.onDragViewAnimationEnd(); 154 } 155 } 156 }); 157 158 mBitmap = bitmap; 159 setDragRegion(new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight())); 160 161 // The point in our scaled bitmap that the touch events are located 162 mRegistrationX = registrationX; 163 mRegistrationY = registrationY; 164 165 mInitialScale = initialScale; 166 mScaleOnDrop = scaleOnDrop; 167 168 // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass 169 int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 170 measure(ms, ms); 171 mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 172 173 mBlurSizeOutline = getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline); 174 setElevation(getResources().getDimension(R.dimen.drag_elevation)); 175 } 176 177 @Override onAttachedToWindow()178 protected void onAttachedToWindow() { 179 super.onAttachedToWindow(); 180 mLauncher.getStateManager().addStateListener(this); 181 } 182 183 @Override onDetachedFromWindow()184 protected void onDetachedFromWindow() { 185 super.onDetachedFromWindow(); 186 mLauncher.getStateManager().removeStateListener(this); 187 } 188 189 @Override onStateTransitionStart(LauncherState toState)190 public void onStateTransitionStart(LauncherState toState) { } 191 192 @Override onStateTransitionComplete(LauncherState finalState)193 public void onStateTransitionComplete(LauncherState finalState) { 194 setVisibility((finalState == LauncherState.NORMAL 195 || finalState == LauncherState.SPRING_LOADED) ? VISIBLE : INVISIBLE); 196 } 197 198 /** 199 * Initialize {@code #mIconDrawable} if the item can be represented using 200 * an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}. 201 */ 202 @TargetApi(Build.VERSION_CODES.O) setItemInfo(final ItemInfo info)203 public void setItemInfo(final ItemInfo info) { 204 if (!Utilities.ATLEAST_OREO) { 205 return; 206 } 207 if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && 208 info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT && 209 info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 210 return; 211 } 212 // Load the adaptive icon on a background thread and add the view in ui thread. 213 MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(new Runnable() { 214 @Override 215 public void run() { 216 Object[] outObj = new Object[1]; 217 int w = mBitmap.getWidth(); 218 int h = mBitmap.getHeight(); 219 Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, 220 false /* flattenDrawable */, outObj); 221 222 if (dr instanceof AdaptiveIconDrawable) { 223 int blurMargin = (int) mLauncher.getResources() 224 .getDimension(R.dimen.blur_size_medium_outline) / 2; 225 226 Rect bounds = new Rect(0, 0, w, h); 227 bounds.inset(blurMargin, blurMargin); 228 // Badge is applied after icon normalization so the bounds for badge should not 229 // be scaled down due to icon normalization. 230 Rect badgeBounds = new Rect(bounds); 231 mBadge = getBadge(mLauncher, info, outObj[0]); 232 mBadge.setBounds(badgeBounds); 233 234 // Do not draw the background in case of folder as its translucent 235 mDrawBitmap = !(dr instanceof FolderAdaptiveIcon); 236 237 try (LauncherIcons li = LauncherIcons.obtain(mLauncher)) { 238 Drawable nDr; // drawable to be normalized 239 if (mDrawBitmap) { 240 nDr = dr; 241 } else { 242 // Since we just want the scale, avoid heavy drawing operations 243 nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null); 244 } 245 Utilities.scaleRectAboutCenter(bounds, 246 li.getNormalizer().getScale(nDr, null, null, null)); 247 } 248 AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr; 249 250 // Shrink very tiny bit so that the clip path is smaller than the original bitmap 251 // that has anti aliased edges and shadows. 252 Rect shrunkBounds = new Rect(bounds); 253 Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f); 254 adaptiveIcon.setBounds(shrunkBounds); 255 final Path mask = adaptiveIcon.getIconMask(); 256 257 mTranslateX = new SpringFloatValue(DragView.this, 258 w * AdaptiveIconDrawable.getExtraInsetFraction()); 259 mTranslateY = new SpringFloatValue(DragView.this, 260 h * AdaptiveIconDrawable.getExtraInsetFraction()); 261 262 bounds.inset( 263 (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()), 264 (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction()) 265 ); 266 mBgSpringDrawable = adaptiveIcon.getBackground(); 267 if (mBgSpringDrawable == null) { 268 mBgSpringDrawable = new ColorDrawable(Color.TRANSPARENT); 269 } 270 mBgSpringDrawable.setBounds(bounds); 271 mFgSpringDrawable = adaptiveIcon.getForeground(); 272 if (mFgSpringDrawable == null) { 273 mFgSpringDrawable = new ColorDrawable(Color.TRANSPARENT); 274 } 275 mFgSpringDrawable.setBounds(bounds); 276 277 new Handler(Looper.getMainLooper()).post(new Runnable() { 278 @Override 279 public void run() { 280 // Assign the variable on the UI thread to avoid race conditions. 281 mScaledMaskPath = mask; 282 283 if (info.isDisabled()) { 284 FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null); 285 d.setIsDisabled(true); 286 mBaseFilter = (ColorMatrixColorFilter) d.getColorFilter(); 287 } 288 updateColorFilter(); 289 } 290 }); 291 } 292 }}); 293 } 294 295 @TargetApi(Build.VERSION_CODES.O) updateColorFilter()296 private void updateColorFilter() { 297 if (mCurrentFilter == null) { 298 mPaint.setColorFilter(null); 299 300 if (mScaledMaskPath != null) { 301 mBgSpringDrawable.setColorFilter(mBaseFilter); 302 mFgSpringDrawable.setColorFilter(mBaseFilter); 303 mBadge.setColorFilter(mBaseFilter); 304 } 305 } else { 306 ColorMatrixColorFilter currentFilter = new ColorMatrixColorFilter(mCurrentFilter); 307 mPaint.setColorFilter(currentFilter); 308 309 if (mScaledMaskPath != null) { 310 if (mBaseFilter != null) { 311 mBaseFilter.getColorMatrix(sTempMatrix1); 312 sTempMatrix2.set(mCurrentFilter); 313 sTempMatrix1.postConcat(sTempMatrix2); 314 315 currentFilter = new ColorMatrixColorFilter(sTempMatrix1); 316 } 317 318 mBgSpringDrawable.setColorFilter(currentFilter); 319 mFgSpringDrawable.setColorFilter(currentFilter); 320 mBadge.setColorFilter(currentFilter); 321 } 322 } 323 324 invalidate(); 325 } 326 327 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)328 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 329 setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight()); 330 } 331 332 /** Sets the scale of the view over the normal workspace icon size. */ setIntrinsicIconScaleFactor(float scale)333 public void setIntrinsicIconScaleFactor(float scale) { 334 mIntrinsicIconScale = scale; 335 } 336 getIntrinsicIconScaleFactor()337 public float getIntrinsicIconScaleFactor() { 338 return mIntrinsicIconScale; 339 } 340 getDragRegionLeft()341 public int getDragRegionLeft() { 342 return mDragRegion.left; 343 } 344 getDragRegionTop()345 public int getDragRegionTop() { 346 return mDragRegion.top; 347 } 348 getDragRegionWidth()349 public int getDragRegionWidth() { 350 return mDragRegion.width(); 351 } 352 getDragRegionHeight()353 public int getDragRegionHeight() { 354 return mDragRegion.height(); 355 } 356 setDragVisualizeOffset(Point p)357 public void setDragVisualizeOffset(Point p) { 358 mDragVisualizeOffset = p; 359 } 360 getDragVisualizeOffset()361 public Point getDragVisualizeOffset() { 362 return mDragVisualizeOffset; 363 } 364 setDragRegion(Rect r)365 public void setDragRegion(Rect r) { 366 mDragRegion = r; 367 } 368 getDragRegion()369 public Rect getDragRegion() { 370 return mDragRegion; 371 } 372 getPreviewBitmap()373 public Bitmap getPreviewBitmap() { 374 return mBitmap; 375 } 376 377 @Override onDraw(Canvas canvas)378 protected void onDraw(Canvas canvas) { 379 mHasDrawn = true; 380 381 if (mDrawBitmap) { 382 // Always draw the bitmap to mask anti aliasing due to clipPath 383 boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null; 384 if (crossFade) { 385 int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255; 386 mPaint.setAlpha(alpha); 387 } 388 canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint); 389 if (crossFade) { 390 mPaint.setAlpha((int) (255 * mCrossFadeProgress)); 391 final int saveCount = canvas.save(); 392 float sX = (mBitmap.getWidth() * 1.0f) / mCrossFadeBitmap.getWidth(); 393 float sY = (mBitmap.getHeight() * 1.0f) / mCrossFadeBitmap.getHeight(); 394 canvas.scale(sX, sY); 395 canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint); 396 canvas.restoreToCount(saveCount); 397 } 398 } 399 400 if (mScaledMaskPath != null) { 401 int cnt = canvas.save(); 402 canvas.clipPath(mScaledMaskPath); 403 mBgSpringDrawable.draw(canvas); 404 canvas.translate(mTranslateX.mValue, mTranslateY.mValue); 405 mFgSpringDrawable.draw(canvas); 406 canvas.restoreToCount(cnt); 407 mBadge.draw(canvas); 408 } 409 } 410 setCrossFadeBitmap(Bitmap crossFadeBitmap)411 public void setCrossFadeBitmap(Bitmap crossFadeBitmap) { 412 mCrossFadeBitmap = crossFadeBitmap; 413 } 414 crossFade(int duration)415 public void crossFade(int duration) { 416 ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); 417 va.setDuration(duration); 418 va.setInterpolator(Interpolators.DEACCEL_1_5); 419 va.addUpdateListener(a -> { 420 mCrossFadeProgress = a.getAnimatedFraction(); 421 invalidate(); 422 }); 423 va.start(); 424 } 425 setColor(int color)426 public void setColor(int color) { 427 if (mPaint == null) { 428 mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 429 } 430 if (color != 0) { 431 ColorMatrix m1 = new ColorMatrix(); 432 m1.setSaturation(0); 433 434 ColorMatrix m2 = new ColorMatrix(); 435 Themes.setColorScaleOnMatrix(color, m2); 436 m1.postConcat(m2); 437 438 animateFilterTo(m1.getArray()); 439 } else { 440 if (mCurrentFilter == null) { 441 updateColorFilter(); 442 } else { 443 animateFilterTo(new ColorMatrix().getArray()); 444 } 445 } 446 } 447 animateFilterTo(float[] targetFilter)448 private void animateFilterTo(float[] targetFilter) { 449 float[] oldFilter = mCurrentFilter == null ? new ColorMatrix().getArray() : mCurrentFilter; 450 mCurrentFilter = Arrays.copyOf(oldFilter, oldFilter.length); 451 452 if (mFilterAnimator != null) { 453 mFilterAnimator.cancel(); 454 } 455 mFilterAnimator = ValueAnimator.ofObject(new FloatArrayEvaluator(mCurrentFilter), 456 oldFilter, targetFilter); 457 mFilterAnimator.setDuration(COLOR_CHANGE_DURATION); 458 mFilterAnimator.addUpdateListener(new AnimatorUpdateListener() { 459 460 @Override 461 public void onAnimationUpdate(ValueAnimator animation) { 462 updateColorFilter(); 463 } 464 }); 465 mFilterAnimator.start(); 466 } 467 hasDrawn()468 public boolean hasDrawn() { 469 return mHasDrawn; 470 } 471 472 @Override setAlpha(float alpha)473 public void setAlpha(float alpha) { 474 super.setAlpha(alpha); 475 mPaint.setAlpha((int) (255 * alpha)); 476 invalidate(); 477 } 478 479 /** 480 * Create a window containing this view and show it. 481 * 482 * @param touchX the x coordinate the user touched in DragLayer coordinates 483 * @param touchY the y coordinate the user touched in DragLayer coordinates 484 */ show(int touchX, int touchY)485 public void show(int touchX, int touchY) { 486 mDragLayer.addView(this); 487 488 // Start the pick-up animation 489 DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0); 490 lp.width = mBitmap.getWidth(); 491 lp.height = mBitmap.getHeight(); 492 lp.customPosition = true; 493 setLayoutParams(lp); 494 move(touchX, touchY); 495 // Post the animation to skip other expensive work happening on the first frame 496 post(new Runnable() { 497 public void run() { 498 mAnim.start(); 499 } 500 }); 501 } 502 cancelAnimation()503 public void cancelAnimation() { 504 mAnimationCancelled = true; 505 if (mAnim != null && mAnim.isRunning()) { 506 mAnim.cancel(); 507 } 508 } 509 510 /** 511 * Move the window containing this view. 512 * 513 * @param touchX the x coordinate the user touched in DragLayer coordinates 514 * @param touchY the y coordinate the user touched in DragLayer coordinates 515 */ move(int touchX, int touchY)516 public void move(int touchX, int touchY) { 517 if (touchX > 0 && touchY > 0 && mLastTouchX > 0 && mLastTouchY > 0 518 && mScaledMaskPath != null) { 519 mTranslateX.animateToPos(mLastTouchX - touchX); 520 mTranslateY.animateToPos(mLastTouchY - touchY); 521 } 522 mLastTouchX = touchX; 523 mLastTouchY = touchY; 524 applyTranslation(); 525 } 526 animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration)527 public void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration) { 528 mTempLoc[0] = toTouchX - mRegistrationX; 529 mTempLoc[1] = toTouchY - mRegistrationY; 530 mDragLayer.animateViewIntoPosition(this, mTempLoc, 1f, mScaleOnDrop, mScaleOnDrop, 531 DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration); 532 } 533 animateShift(final int shiftX, final int shiftY)534 public void animateShift(final int shiftX, final int shiftY) { 535 if (mAnim.isStarted()) { 536 return; 537 } 538 mAnimatedShiftX = shiftX; 539 mAnimatedShiftY = shiftY; 540 applyTranslation(); 541 mAnim.addUpdateListener(new AnimatorUpdateListener() { 542 @Override 543 public void onAnimationUpdate(ValueAnimator animation) { 544 float fraction = 1 - animation.getAnimatedFraction(); 545 mAnimatedShiftX = (int) (fraction * shiftX); 546 mAnimatedShiftY = (int) (fraction * shiftY); 547 applyTranslation(); 548 } 549 }); 550 } 551 applyTranslation()552 private void applyTranslation() { 553 setTranslationX(mLastTouchX - mRegistrationX + mAnimatedShiftX); 554 setTranslationY(mLastTouchY - mRegistrationY + mAnimatedShiftY); 555 } 556 remove()557 public void remove() { 558 if (getParent() != null) { 559 mDragLayer.removeView(DragView.this); 560 } 561 } 562 getBlurSizeOutline()563 public int getBlurSizeOutline() { 564 return mBlurSizeOutline; 565 } 566 getInitialScale()567 public float getInitialScale() { 568 return mInitialScale; 569 } 570 571 private static class SpringFloatValue { 572 573 private static final FloatPropertyCompat<SpringFloatValue> VALUE = 574 new FloatPropertyCompat<SpringFloatValue>("value") { 575 @Override 576 public float getValue(SpringFloatValue object) { 577 return object.mValue; 578 } 579 580 @Override 581 public void setValue(SpringFloatValue object, float value) { 582 object.mValue = value; 583 object.mView.invalidate(); 584 } 585 }; 586 587 // Following three values are fine tuned with motion ux designer 588 private final static int STIFFNESS = 4000; 589 private final static float DAMPENING_RATIO = 1f; 590 private final static int PARALLAX_MAX_IN_DP = 8; 591 592 private final View mView; 593 private final SpringAnimation mSpring; 594 private final float mDelta; 595 596 private float mValue; 597 SpringFloatValue(View view, float range)598 public SpringFloatValue(View view, float range) { 599 mView = view; 600 mSpring = new SpringAnimation(this, VALUE, 0) 601 .setMinValue(-range).setMaxValue(range) 602 .setSpring(new SpringForce(0) 603 .setDampingRatio(DAMPENING_RATIO) 604 .setStiffness(STIFFNESS)); 605 mDelta = view.getResources().getDisplayMetrics().density * PARALLAX_MAX_IN_DP; 606 } 607 animateToPos(float value)608 public void animateToPos(float value) { 609 mSpring.animateToFinalPosition(Utilities.boundToRange(value, -mDelta, mDelta)); 610 } 611 } 612 } 613