1 /* 2 * Copyright (C) 2013 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.camera.widget; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorSet; 21 import android.animation.TimeInterpolator; 22 import android.animation.ValueAnimator; 23 import android.annotation.TargetApi; 24 import android.app.Activity; 25 import android.content.Context; 26 import android.graphics.Canvas; 27 import android.graphics.Point; 28 import android.graphics.Rect; 29 import android.graphics.RectF; 30 import android.net.Uri; 31 import android.os.Build; 32 import android.os.Bundle; 33 import android.os.Handler; 34 import android.os.SystemClock; 35 import android.util.AttributeSet; 36 import android.util.DisplayMetrics; 37 import android.util.SparseArray; 38 import android.view.MotionEvent; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.accessibility.AccessibilityNodeInfo; 42 import android.view.animation.DecelerateInterpolator; 43 import android.widget.Scroller; 44 45 import com.android.camera.CameraActivity; 46 import com.android.camera.data.LocalData.ActionCallback; 47 import com.android.camera.debug.Log; 48 import com.android.camera.filmstrip.DataAdapter; 49 import com.android.camera.filmstrip.FilmstripController; 50 import com.android.camera.filmstrip.ImageData; 51 import com.android.camera.ui.FilmstripGestureRecognizer; 52 import com.android.camera.ui.ZoomView; 53 import com.android.camera.util.ApiHelper; 54 import com.android.camera.util.CameraUtil; 55 import com.android.camera2.R; 56 57 import java.lang.ref.WeakReference; 58 import java.util.ArrayDeque; 59 import java.util.Arrays; 60 import java.util.Queue; 61 62 public class FilmstripView extends ViewGroup { 63 /** 64 * An action callback to be used for actions on the local media data items. 65 */ 66 public static class ActionCallbackImpl implements ActionCallback { 67 private final WeakReference<Activity> mActivity; 68 69 /** 70 * The given activity is used to start intents. It is wrapped in a weak 71 * reference to prevent leaks. 72 */ ActionCallbackImpl(Activity activity)73 public ActionCallbackImpl(Activity activity) { 74 mActivity = new WeakReference<Activity>(activity); 75 } 76 77 /** 78 * Fires an intent to play the video with the given URI and title. 79 */ 80 @Override playVideo(Uri uri, String title)81 public void playVideo(Uri uri, String title) { 82 Activity activity = mActivity.get(); 83 if (activity != null) { 84 CameraUtil.playVideo(activity, uri, title); 85 } 86 } 87 } 88 89 90 private static final Log.Tag TAG = new Log.Tag("FilmstripView"); 91 92 private static final int BUFFER_SIZE = 5; 93 private static final int GEOMETRY_ADJUST_TIME_MS = 400; 94 private static final int SNAP_IN_CENTER_TIME_MS = 600; 95 private static final float FLING_COASTING_DURATION_S = 0.05f; 96 private static final int ZOOM_ANIMATION_DURATION_MS = 200; 97 private static final int CAMERA_PREVIEW_SWIPE_THRESHOLD = 300; 98 private static final float FILM_STRIP_SCALE = 0.7f; 99 private static final float FULL_SCREEN_SCALE = 1f; 100 101 // The min velocity at which the user must have moved their finger in 102 // pixels per millisecond to count a vertical gesture as a promote/demote 103 // at short vertical distances. 104 private static final float PROMOTE_VELOCITY = 3.5f; 105 // The min distance relative to this view's height the user must have 106 // moved their finger to count a vertical gesture as a promote/demote if 107 // they moved their finger at least at PROMOTE_VELOCITY. 108 private static final float VELOCITY_PROMOTE_HEIGHT_RATIO = 1/10f; 109 // The min distance relative to this view's height the user must have 110 // moved their finger to count a vertical gesture as a promote/demote if 111 // they moved their finger at less than PROMOTE_VELOCITY. 112 private static final float PROMOTE_HEIGHT_RATIO = 1/2f; 113 114 private static final float TOLERANCE = 0.1f; 115 // Only check for intercepting touch events within first 500ms 116 private static final int SWIPE_TIME_OUT = 500; 117 private static final int DECELERATION_FACTOR = 4; 118 119 private CameraActivity mActivity; 120 private ActionCallback mActionCallback; 121 private FilmstripGestureRecognizer mGestureRecognizer; 122 private FilmstripGestureRecognizer.Listener mGestureListener; 123 private DataAdapter mDataAdapter; 124 private int mViewGapInPixel; 125 private final Rect mDrawArea = new Rect(); 126 127 private final int mCurrentItem = (BUFFER_SIZE - 1) / 2; 128 private float mScale; 129 private MyController mController; 130 private int mCenterX = -1; 131 private final ViewItem[] mViewItem = new ViewItem[BUFFER_SIZE]; 132 133 private FilmstripController.FilmstripListener mListener; 134 private ZoomView mZoomView = null; 135 136 private MotionEvent mDown; 137 private boolean mCheckToIntercept = true; 138 private int mSlop; 139 private TimeInterpolator mViewAnimInterpolator; 140 141 // This is true if and only if the user is scrolling, 142 private boolean mIsUserScrolling; 143 private int mDataIdOnUserScrolling; 144 private float mOverScaleFactor = 1f; 145 146 private boolean mFullScreenUIHidden = false; 147 private final SparseArray<Queue<View>> recycledViews = new SparseArray<Queue<View>>(); 148 149 /** 150 * A helper class to tract and calculate the view coordination. 151 */ 152 private class ViewItem { 153 private int mDataId; 154 /** The position of the left of the view in the whole filmstrip. */ 155 private int mLeftPosition; 156 private final View mView; 157 private final ImageData mData; 158 private final RectF mViewArea; 159 private boolean mMaximumBitmapRequested; 160 161 private ValueAnimator mTranslationXAnimator; 162 private ValueAnimator mTranslationYAnimator; 163 private ValueAnimator mAlphaAnimator; 164 165 /** 166 * Constructor. 167 * 168 * @param id The id of the data from 169 * {@link com.android.camera.filmstrip.DataAdapter}. 170 * @param v The {@code View} representing the data. 171 */ ViewItem(int id, View v, ImageData data)172 public ViewItem(int id, View v, ImageData data) { 173 v.setPivotX(0f); 174 v.setPivotY(0f); 175 mDataId = id; 176 mData = data; 177 mView = v; 178 mMaximumBitmapRequested = false; 179 mLeftPosition = -1; 180 mViewArea = new RectF(); 181 } 182 isMaximumBitmapRequested()183 public boolean isMaximumBitmapRequested() { 184 return mMaximumBitmapRequested; 185 } 186 setMaximumBitmapRequested()187 public void setMaximumBitmapRequested() { 188 mMaximumBitmapRequested = true; 189 } 190 191 /** 192 * Returns the data id from 193 * {@link com.android.camera.filmstrip.DataAdapter}. 194 */ getId()195 public int getId() { 196 return mDataId; 197 } 198 199 /** 200 * Sets the data id from 201 * {@link com.android.camera.filmstrip.DataAdapter}. 202 */ setId(int id)203 public void setId(int id) { 204 mDataId = id; 205 } 206 207 /** Sets the left position of the view in the whole filmstrip. */ setLeftPosition(int pos)208 public void setLeftPosition(int pos) { 209 mLeftPosition = pos; 210 } 211 212 /** Returns the left position of the view in the whole filmstrip. */ getLeftPosition()213 public int getLeftPosition() { 214 return mLeftPosition; 215 } 216 217 /** Returns the translation of Y regarding the view scale. */ getTranslationY()218 public float getTranslationY() { 219 return mView.getTranslationY() / mScale; 220 } 221 222 /** Returns the translation of X regarding the view scale. */ getTranslationX()223 public float getTranslationX() { 224 return mView.getTranslationX() / mScale; 225 } 226 227 /** Sets the translation of Y regarding the view scale. */ setTranslationY(float transY)228 public void setTranslationY(float transY) { 229 mView.setTranslationY(transY * mScale); 230 } 231 232 /** Sets the translation of X regarding the view scale. */ setTranslationX(float transX)233 public void setTranslationX(float transX) { 234 mView.setTranslationX(transX * mScale); 235 } 236 237 /** Forwarding of {@link android.view.View#setAlpha(float)}. */ setAlpha(float alpha)238 public void setAlpha(float alpha) { 239 mView.setAlpha(alpha); 240 } 241 242 /** Forwarding of {@link android.view.View#getAlpha()}. */ getAlpha()243 public float getAlpha() { 244 return mView.getAlpha(); 245 } 246 247 /** Forwarding of {@link android.view.View#getMeasuredWidth()}. */ getMeasuredWidth()248 public int getMeasuredWidth() { 249 return mView.getMeasuredWidth(); 250 } 251 252 /** 253 * Animates the X translation of the view. Note: the animated value is 254 * not set directly by {@link android.view.View#setTranslationX(float)} 255 * because the value might be changed during in {@code onLayout()}. 256 * The animated value of X translation is specially handled in {@code 257 * layoutIn()}. 258 * 259 * @param targetX The final value. 260 * @param duration_ms The duration of the animation. 261 * @param interpolator Time interpolator. 262 */ animateTranslationX( float targetX, long duration_ms, TimeInterpolator interpolator)263 public void animateTranslationX( 264 float targetX, long duration_ms, TimeInterpolator interpolator) { 265 if (mTranslationXAnimator == null) { 266 mTranslationXAnimator = new ValueAnimator(); 267 mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 268 @Override 269 public void onAnimationUpdate(ValueAnimator valueAnimator) { 270 // We invalidate the filmstrip view instead of setting the 271 // translation X because the translation X of the view is 272 // touched in onLayout(). See the documentation of 273 // animateTranslationX(). 274 invalidate(); 275 } 276 }); 277 } 278 runAnimation(mTranslationXAnimator, getTranslationX(), targetX, duration_ms, 279 interpolator); 280 } 281 282 /** 283 * Animates the Y translation of the view. 284 * 285 * @param targetY The final value. 286 * @param duration_ms The duration of the animation. 287 * @param interpolator Time interpolator. 288 */ animateTranslationY( float targetY, long duration_ms, TimeInterpolator interpolator)289 public void animateTranslationY( 290 float targetY, long duration_ms, TimeInterpolator interpolator) { 291 if (mTranslationYAnimator == null) { 292 mTranslationYAnimator = new ValueAnimator(); 293 mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 294 @Override 295 public void onAnimationUpdate(ValueAnimator valueAnimator) { 296 setTranslationY((Float) valueAnimator.getAnimatedValue()); 297 } 298 }); 299 } 300 runAnimation(mTranslationYAnimator, getTranslationY(), targetY, duration_ms, 301 interpolator); 302 } 303 304 /** 305 * Animates the alpha value of the view. 306 * 307 * @param targetAlpha The final value. 308 * @param duration_ms The duration of the animation. 309 * @param interpolator Time interpolator. 310 */ animateAlpha(float targetAlpha, long duration_ms, TimeInterpolator interpolator)311 public void animateAlpha(float targetAlpha, long duration_ms, 312 TimeInterpolator interpolator) { 313 if (mAlphaAnimator == null) { 314 mAlphaAnimator = new ValueAnimator(); 315 mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 316 @Override 317 public void onAnimationUpdate(ValueAnimator valueAnimator) { 318 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue()); 319 } 320 }); 321 } 322 runAnimation(mAlphaAnimator, getAlpha(), targetAlpha, duration_ms, interpolator); 323 } 324 runAnimation(final ValueAnimator animator, final float startValue, final float targetValue, final long duration_ms, final TimeInterpolator interpolator)325 private void runAnimation(final ValueAnimator animator, final float startValue, 326 final float targetValue, final long duration_ms, 327 final TimeInterpolator interpolator) { 328 if (startValue == targetValue) { 329 return; 330 } 331 animator.setInterpolator(interpolator); 332 animator.setDuration(duration_ms); 333 animator.setFloatValues(startValue, targetValue); 334 animator.start(); 335 } 336 337 /** Adjusts the translation of X regarding the view scale. */ translateXScaledBy(float transX)338 public void translateXScaledBy(float transX) { 339 setTranslationX(getTranslationX() + transX * mScale); 340 } 341 342 /** 343 * Forwarding of {@link android.view.View#getHitRect(android.graphics.Rect)}. 344 */ getHitRect(Rect rect)345 public void getHitRect(Rect rect) { 346 mView.getHitRect(rect); 347 } 348 getCenterX()349 public int getCenterX() { 350 return mLeftPosition + mView.getMeasuredWidth() / 2; 351 } 352 353 /** Forwarding of {@link android.view.View#getVisibility()}. */ getVisibility()354 public int getVisibility() { 355 return mView.getVisibility(); 356 } 357 358 /** Forwarding of {@link android.view.View#setVisibility(int)}. */ setVisibility(int visibility)359 public void setVisibility(int visibility) { 360 mView.setVisibility(visibility); 361 } 362 363 /** 364 * Notifies the {@link com.android.camera.filmstrip.DataAdapter} to 365 * resize the view. 366 */ resizeView(Context context, int w, int h)367 public void resizeView(Context context, int w, int h) { 368 mDataAdapter.resizeView(context, mDataId, mView, w, h); 369 } 370 371 /** 372 * Adds the view of the data to the view hierarchy if necessary. 373 */ addViewToHierarchy()374 public void addViewToHierarchy() { 375 if (indexOfChild(mView) < 0) { 376 mData.prepare(); 377 addView(mView); 378 } 379 380 setVisibility(View.VISIBLE); 381 setAlpha(1f); 382 setTranslationX(0); 383 setTranslationY(0); 384 } 385 386 /** 387 * Removes from the hierarchy. Keeps the view in the view hierarchy if 388 * view type is {@code VIEW_TYPE_STICKY} and set to invisible instead. 389 * 390 * @param force {@code true} to remove the view from the hierarchy 391 * regardless of the view type. 392 */ removeViewFromHierarchy(boolean force)393 public void removeViewFromHierarchy(boolean force) { 394 if (force || mData.getViewType() != ImageData.VIEW_TYPE_STICKY) { 395 removeView(mView); 396 mData.recycle(mView); 397 recycleView(mView, mDataId); 398 } else { 399 setVisibility(View.INVISIBLE); 400 } 401 } 402 403 /** 404 * Brings the view to front by 405 * {@link #bringChildToFront(android.view.View)} 406 */ bringViewToFront()407 public void bringViewToFront() { 408 bringChildToFront(mView); 409 } 410 411 /** 412 * The visual x position of this view, in pixels. 413 */ getX()414 public float getX() { 415 return mView.getX(); 416 } 417 418 /** 419 * The visual y position of this view, in pixels. 420 */ getY()421 public float getY() { 422 return mView.getY(); 423 } 424 425 /** 426 * Forwarding of {@link android.view.View#measure(int, int)}. 427 */ measure(int widthSpec, int heightSpec)428 public void measure(int widthSpec, int heightSpec) { 429 mView.measure(widthSpec, heightSpec); 430 } 431 layoutAt(int left, int top)432 private void layoutAt(int left, int top) { 433 mView.layout(left, top, left + mView.getMeasuredWidth(), 434 top + mView.getMeasuredHeight()); 435 } 436 437 /** 438 * The bounding rect of the view. 439 */ getViewRect()440 public RectF getViewRect() { 441 RectF r = new RectF(); 442 r.left = mView.getX(); 443 r.top = mView.getY(); 444 r.right = r.left + mView.getWidth() * mView.getScaleX(); 445 r.bottom = r.top + mView.getHeight() * mView.getScaleY(); 446 return r; 447 } 448 getView()449 private View getView() { 450 return mView; 451 } 452 453 /** 454 * Layouts the view in the area assuming the center of the area is at a 455 * specific point of the whole filmstrip. 456 * 457 * @param drawArea The area when filmstrip will show in. 458 * @param refCenter The absolute X coordination in the whole filmstrip 459 * of the center of {@code drawArea}. 460 * @param scale The scale of the view on the filmstrip. 461 */ layoutWithTranslationX(Rect drawArea, int refCenter, float scale)462 public void layoutWithTranslationX(Rect drawArea, int refCenter, float scale) { 463 final float translationX = 464 ((mTranslationXAnimator != null && mTranslationXAnimator.isRunning()) ? 465 (Float) mTranslationXAnimator.getAnimatedValue() : 0); 466 int left = 467 (int) (drawArea.centerX() + (mLeftPosition - refCenter + translationX) * scale); 468 int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale); 469 layoutAt(left, top); 470 mView.setScaleX(scale); 471 mView.setScaleY(scale); 472 473 // update mViewArea for touch detection. 474 int l = mView.getLeft(); 475 int t = mView.getTop(); 476 mViewArea.set(l, t, 477 l + mView.getMeasuredWidth() * scale, 478 t + mView.getMeasuredHeight() * scale); 479 } 480 481 /** Returns true if the point is in the view. */ areaContains(float x, float y)482 public boolean areaContains(float x, float y) { 483 return mViewArea.contains(x, y); 484 } 485 486 /** 487 * Return the width of the view. 488 */ getWidth()489 public int getWidth() { 490 return mView.getWidth(); 491 } 492 493 /** 494 * Returns the position of the left edge of the view area content is drawn in. 495 */ getDrawAreaLeft()496 public int getDrawAreaLeft() { 497 return Math.round(mViewArea.left); 498 } 499 copyAttributes(ViewItem item)500 public void copyAttributes(ViewItem item) { 501 setLeftPosition(item.getLeftPosition()); 502 // X 503 setTranslationX(item.getTranslationX()); 504 if (item.mTranslationXAnimator != null) { 505 mTranslationXAnimator = item.mTranslationXAnimator; 506 mTranslationXAnimator.removeAllUpdateListeners(); 507 mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 508 @Override 509 public void onAnimationUpdate(ValueAnimator valueAnimator) { 510 // We invalidate the filmstrip view instead of setting the 511 // translation X because the translation X of the view is 512 // touched in onLayout(). See the documentation of 513 // animateTranslationX(). 514 invalidate(); 515 } 516 }); 517 } 518 // Y 519 setTranslationY(item.getTranslationY()); 520 if (item.mTranslationYAnimator != null) { 521 mTranslationYAnimator = item.mTranslationYAnimator; 522 mTranslationYAnimator.removeAllUpdateListeners(); 523 mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 524 @Override 525 public void onAnimationUpdate(ValueAnimator valueAnimator) { 526 setTranslationY((Float) valueAnimator.getAnimatedValue()); 527 } 528 }); 529 } 530 // Alpha 531 setAlpha(item.getAlpha()); 532 if (item.mAlphaAnimator != null) { 533 mAlphaAnimator = item.mAlphaAnimator; 534 mAlphaAnimator.removeAllUpdateListeners(); 535 mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 536 @Override 537 public void onAnimationUpdate(ValueAnimator valueAnimator) { 538 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue()); 539 } 540 }); 541 } 542 } 543 544 /** 545 * Apply a scale factor (i.e. {@code postScale}) on top of current scale at 546 * pivot point ({@code focusX}, {@code focusY}). Visually it should be the 547 * same as post concatenating current view's matrix with specified scale. 548 */ postScale(float focusX, float focusY, float postScale, int viewportWidth, int viewportHeight)549 void postScale(float focusX, float focusY, float postScale, int viewportWidth, 550 int viewportHeight) { 551 float transX = mView.getTranslationX(); 552 float transY = mView.getTranslationY(); 553 // Pivot point is top left of the view, so we need to translate 554 // to scale around focus point 555 transX -= (focusX - getX()) * (postScale - 1f); 556 transY -= (focusY - getY()) * (postScale - 1f); 557 float scaleX = mView.getScaleX() * postScale; 558 float scaleY = mView.getScaleY() * postScale; 559 updateTransform(transX, transY, scaleX, scaleY, viewportWidth, 560 viewportHeight); 561 } 562 updateTransform(float transX, float transY, float scaleX, float scaleY, int viewportWidth, int viewportHeight)563 void updateTransform(float transX, float transY, float scaleX, float scaleY, 564 int viewportWidth, int viewportHeight) { 565 float left = transX + mView.getLeft(); 566 float top = transY + mView.getTop(); 567 RectF r = ZoomView.adjustToFitInBounds(new RectF(left, top, 568 left + mView.getWidth() * scaleX, 569 top + mView.getHeight() * scaleY), 570 viewportWidth, viewportHeight); 571 mView.setScaleX(scaleX); 572 mView.setScaleY(scaleY); 573 transX = r.left - mView.getLeft(); 574 transY = r.top - mView.getTop(); 575 mView.setTranslationX(transX); 576 mView.setTranslationY(transY); 577 } 578 resetTransform()579 void resetTransform() { 580 mView.setScaleX(FULL_SCREEN_SCALE); 581 mView.setScaleY(FULL_SCREEN_SCALE); 582 mView.setTranslationX(0f); 583 mView.setTranslationY(0f); 584 } 585 586 @Override toString()587 public String toString() { 588 return "DataID = " + mDataId + "\n\t left = " + mLeftPosition 589 + "\n\t viewArea = " + mViewArea 590 + "\n\t centerX = " + getCenterX() 591 + "\n\t view MeasuredSize = " 592 + mView.getMeasuredWidth() + ',' + mView.getMeasuredHeight() 593 + "\n\t view Size = " + mView.getWidth() + ',' + mView.getHeight() 594 + "\n\t view scale = " + mView.getScaleX(); 595 } 596 } 597 598 /** Constructor. */ FilmstripView(Context context)599 public FilmstripView(Context context) { 600 super(context); 601 init((CameraActivity) context); 602 } 603 604 /** Constructor. */ FilmstripView(Context context, AttributeSet attrs)605 public FilmstripView(Context context, AttributeSet attrs) { 606 super(context, attrs); 607 init((CameraActivity) context); 608 } 609 610 /** Constructor. */ FilmstripView(Context context, AttributeSet attrs, int defStyle)611 public FilmstripView(Context context, AttributeSet attrs, int defStyle) { 612 super(context, attrs, defStyle); 613 init((CameraActivity) context); 614 } 615 init(CameraActivity cameraActivity)616 private void init(CameraActivity cameraActivity) { 617 setWillNotDraw(false); 618 mActivity = cameraActivity; 619 mActionCallback = new ActionCallbackImpl(mActivity); 620 mScale = 1.0f; 621 mDataIdOnUserScrolling = 0; 622 mController = new MyController(cameraActivity); 623 mViewAnimInterpolator = new DecelerateInterpolator(); 624 mZoomView = new ZoomView(cameraActivity); 625 mZoomView.setVisibility(GONE); 626 addView(mZoomView); 627 628 mGestureListener = new MyGestureReceiver(); 629 mGestureRecognizer = 630 new FilmstripGestureRecognizer(cameraActivity, mGestureListener); 631 mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop); 632 DisplayMetrics metrics = new DisplayMetrics(); 633 mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics); 634 // Allow over scaling because on high density screens, pixels are too 635 // tiny to clearly see the details at 1:1 zoom. We should not scale 636 // beyond what 1:1 would look like on a medium density screen, as 637 // scaling beyond that would only yield blur. 638 mOverScaleFactor = (float) metrics.densityDpi / (float) DisplayMetrics.DENSITY_HIGH; 639 if (mOverScaleFactor < 1f) { 640 mOverScaleFactor = 1f; 641 } 642 643 setAccessibilityDelegate(new AccessibilityDelegate() { 644 @Override 645 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 646 super.onInitializeAccessibilityNodeInfo(host, info); 647 648 info.setClassName(FilmstripView.class.getName()); 649 info.setScrollable(true); 650 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 651 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 652 } 653 654 @Override 655 public boolean performAccessibilityAction(View host, int action, Bundle args) { 656 if (!mController.isScrolling()) { 657 switch (action) { 658 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 659 mController.goToNextItem(); 660 return true; 661 } 662 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 663 boolean wentToPrevious = mController.goToPreviousItem(); 664 if (!wentToPrevious) { 665 // at beginning of filmstrip, hide and go back to preview 666 mActivity.getCameraAppUI().hideFilmstrip(); 667 } 668 return true; 669 } 670 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { 671 // Prevent the view group itself from being selected. 672 // Instead, select the item in the center 673 final ViewItem currentItem = mViewItem[mCurrentItem]; 674 currentItem.getView().performAccessibilityAction(action, args); 675 return true; 676 } 677 } 678 } 679 return super.performAccessibilityAction(host, action, args); 680 } 681 }); 682 } 683 recycleView(View view, int dataId)684 private void recycleView(View view, int dataId) { 685 final int viewType = (Integer) view.getTag(R.id.mediadata_tag_viewtype); 686 if (viewType > 0) { 687 Queue<View> recycledViewsForType = recycledViews.get(viewType); 688 if (recycledViewsForType == null) { 689 recycledViewsForType = new ArrayDeque<View>(); 690 recycledViews.put(viewType, recycledViewsForType); 691 } 692 recycledViewsForType.offer(view); 693 } 694 } 695 getRecycledView(int dataId)696 private View getRecycledView(int dataId) { 697 final int viewType = mDataAdapter.getItemViewType(dataId); 698 Queue<View> recycledViewsForType = recycledViews.get(viewType); 699 View result = null; 700 if (recycledViewsForType != null) { 701 result = recycledViewsForType.poll(); 702 } 703 return result; 704 } 705 706 /** 707 * Returns the controller. 708 * 709 * @return The {@code Controller}. 710 */ getController()711 public FilmstripController getController() { 712 return mController; 713 } 714 715 /** 716 * Returns the draw area width of the current item. 717 */ getCurrentItemLeft()718 public int getCurrentItemLeft() { 719 return mViewItem[mCurrentItem].getDrawAreaLeft(); 720 } 721 setListener(FilmstripController.FilmstripListener l)722 private void setListener(FilmstripController.FilmstripListener l) { 723 mListener = l; 724 } 725 setViewGap(int viewGap)726 private void setViewGap(int viewGap) { 727 mViewGapInPixel = viewGap; 728 } 729 730 /** 731 * Called after current item or zoom level has changed. 732 */ zoomAtIndexChanged()733 public void zoomAtIndexChanged() { 734 if (mViewItem[mCurrentItem] == null) { 735 return; 736 } 737 int id = mViewItem[mCurrentItem].getId(); 738 mListener.onZoomAtIndexChanged(id, mScale); 739 } 740 741 /** 742 * Checks if the data is at the center. 743 * 744 * @param id The id of the data to check. 745 * @return {@code True} if the data is currently at the center. 746 */ isDataAtCenter(int id)747 private boolean isDataAtCenter(int id) { 748 if (mViewItem[mCurrentItem] == null) { 749 return false; 750 } 751 if (mViewItem[mCurrentItem].getId() == id 752 && isCurrentItemCentered()) { 753 return true; 754 } 755 return false; 756 } 757 measureViewItem(ViewItem item, int boundWidth, int boundHeight)758 private void measureViewItem(ViewItem item, int boundWidth, int boundHeight) { 759 int id = item.getId(); 760 ImageData imageData = mDataAdapter.getImageData(id); 761 if (imageData == null) { 762 Log.e(TAG, "trying to measure a null item"); 763 return; 764 } 765 766 Point dim = CameraUtil.resizeToFill(imageData.getWidth(), imageData.getHeight(), 767 imageData.getRotation(), boundWidth, boundHeight); 768 769 item.measure(MeasureSpec.makeMeasureSpec(dim.x, MeasureSpec.EXACTLY), 770 MeasureSpec.makeMeasureSpec(dim.y, MeasureSpec.EXACTLY)); 771 } 772 773 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)774 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 775 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 776 777 int boundWidth = MeasureSpec.getSize(widthMeasureSpec); 778 int boundHeight = MeasureSpec.getSize(heightMeasureSpec); 779 if (boundWidth == 0 || boundHeight == 0) { 780 // Either width or height is unknown, can't measure children yet. 781 return; 782 } 783 784 for (ViewItem item : mViewItem) { 785 if (item != null) { 786 measureViewItem(item, boundWidth, boundHeight); 787 } 788 } 789 clampCenterX(); 790 // Measure zoom view 791 mZoomView.measure(MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.EXACTLY), 792 MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.EXACTLY)); 793 } 794 findTheNearestView(int pointX)795 private int findTheNearestView(int pointX) { 796 797 int nearest = 0; 798 // Find the first non-null ViewItem. 799 while (nearest < BUFFER_SIZE 800 && (mViewItem[nearest] == null || mViewItem[nearest].getLeftPosition() == -1)) { 801 nearest++; 802 } 803 // No existing available ViewItem 804 if (nearest == BUFFER_SIZE) { 805 return -1; 806 } 807 808 int min = Math.abs(pointX - mViewItem[nearest].getCenterX()); 809 810 for (int itemID = nearest + 1; itemID < BUFFER_SIZE && mViewItem[itemID] != null; itemID++) { 811 // Not measured yet. 812 if (mViewItem[itemID].getLeftPosition() == -1) { 813 continue; 814 } 815 816 int c = mViewItem[itemID].getCenterX(); 817 int dist = Math.abs(pointX - c); 818 if (dist < min) { 819 min = dist; 820 nearest = itemID; 821 } 822 } 823 return nearest; 824 } 825 buildItemFromData(int dataID)826 private ViewItem buildItemFromData(int dataID) { 827 if (mActivity.isDestroyed()) { 828 // Loading item data is call from multiple AsyncTasks and the 829 // activity may be finished when buildItemFromData is called. 830 Log.d(TAG, "Activity destroyed, don't load data"); 831 return null; 832 } 833 ImageData data = mDataAdapter.getImageData(dataID); 834 if (data == null) { 835 return null; 836 } 837 838 // Always scale by fixed filmstrip scale, since we only show items when 839 // in filmstrip. Preloading images with a different scale and bounds 840 // interferes with caching. 841 int width = Math.round(FILM_STRIP_SCALE * getWidth()); 842 int height = Math.round(FILM_STRIP_SCALE * getHeight()); 843 Log.v(TAG, "suggesting item bounds: " + width + "x" + height); 844 mDataAdapter.suggestViewSizeBound(width, height); 845 846 data.prepare(); 847 View recycled = getRecycledView(dataID); 848 View v = mDataAdapter.getView(mActivity.getAndroidContext(), recycled, dataID, 849 mActionCallback); 850 if (v == null) { 851 return null; 852 } 853 ViewItem item = new ViewItem(dataID, v, data); 854 item.addViewToHierarchy(); 855 return item; 856 } 857 checkItemAtMaxSize()858 private void checkItemAtMaxSize() { 859 ViewItem item = mViewItem[mCurrentItem]; 860 if (item.isMaximumBitmapRequested()) { 861 return; 862 }; 863 item.setMaximumBitmapRequested(); 864 // Request full size bitmap, or max that DataAdapter will create. 865 int id = item.getId(); 866 int h = mDataAdapter.getImageData(id).getHeight(); 867 int w = mDataAdapter.getImageData(id).getWidth(); 868 item.resizeView(mActivity, w, h); 869 } 870 removeItem(int itemID)871 private void removeItem(int itemID) { 872 if (itemID >= mViewItem.length || mViewItem[itemID] == null) { 873 return; 874 } 875 ImageData data = mDataAdapter.getImageData(mViewItem[itemID].getId()); 876 if (data == null) { 877 Log.e(TAG, "trying to remove a null item"); 878 return; 879 } 880 mViewItem[itemID].removeViewFromHierarchy(false); 881 mViewItem[itemID] = null; 882 } 883 884 /** 885 * We try to keep the one closest to the center of the screen at position 886 * mCurrentItem. 887 */ stepIfNeeded()888 private void stepIfNeeded() { 889 if (!inFilmstrip() && !inFullScreen()) { 890 // The good timing to step to the next view is when everything is 891 // not in transition. 892 return; 893 } 894 final int nearest = findTheNearestView(mCenterX); 895 // no change made. 896 if (nearest == -1 || nearest == mCurrentItem) { 897 return; 898 } 899 int prevDataId = (mViewItem[mCurrentItem] == null ? -1 : mViewItem[mCurrentItem].getId()); 900 final int adjust = nearest - mCurrentItem; 901 if (adjust > 0) { 902 for (int k = 0; k < adjust; k++) { 903 removeItem(k); 904 } 905 for (int k = 0; k + adjust < BUFFER_SIZE; k++) { 906 mViewItem[k] = mViewItem[k + adjust]; 907 } 908 for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) { 909 mViewItem[k] = null; 910 if (mViewItem[k - 1] != null) { 911 mViewItem[k] = buildItemFromData(mViewItem[k - 1].getId() + 1); 912 } 913 } 914 adjustChildZOrder(); 915 } else { 916 for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) { 917 removeItem(k); 918 } 919 for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) { 920 mViewItem[k] = mViewItem[k + adjust]; 921 } 922 for (int k = -1 - adjust; k >= 0; k--) { 923 mViewItem[k] = null; 924 if (mViewItem[k + 1] != null) { 925 mViewItem[k] = buildItemFromData(mViewItem[k + 1].getId() - 1); 926 } 927 } 928 } 929 invalidate(); 930 if (mListener != null) { 931 mListener.onDataFocusChanged(prevDataId, mViewItem[mCurrentItem].getId()); 932 final int firstVisible = mViewItem[mCurrentItem].getId() - 2; 933 final int visibleItemCount = firstVisible + BUFFER_SIZE; 934 final int totalItemCount = mDataAdapter.getTotalNumber(); 935 mListener.onScroll(firstVisible, visibleItemCount, totalItemCount); 936 } 937 zoomAtIndexChanged(); 938 } 939 940 /** 941 * Check the bounds of {@code mCenterX}. Always call this function after: 1. 942 * Any changes to {@code mCenterX}. 2. Any size change of the view items. 943 * 944 * @return Whether clamp happened. 945 */ clampCenterX()946 private boolean clampCenterX() { 947 ViewItem curr = mViewItem[mCurrentItem]; 948 if (curr == null) { 949 return false; 950 } 951 952 boolean stopScroll = false; 953 if (curr.getId() == 1 && mCenterX < curr.getCenterX() && mDataIdOnUserScrolling > 1 && 954 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY && 955 mController.isScrolling()) { 956 stopScroll = true; 957 } else { 958 if (curr.getId() == 0 && mCenterX < curr.getCenterX()) { 959 // Stop at the first ViewItem. 960 stopScroll = true; 961 } 962 } 963 if (curr.getId() == mDataAdapter.getTotalNumber() - 1 964 && mCenterX > curr.getCenterX()) { 965 // Stop at the end. 966 stopScroll = true; 967 } 968 969 if (stopScroll) { 970 mCenterX = curr.getCenterX(); 971 } 972 973 return stopScroll; 974 } 975 976 /** 977 * Reorders the child views to be consistent with their data ID. This method 978 * should be called after adding/removing views. 979 */ adjustChildZOrder()980 private void adjustChildZOrder() { 981 for (int i = BUFFER_SIZE - 1; i >= 0; i--) { 982 if (mViewItem[i] == null) { 983 continue; 984 } 985 mViewItem[i].bringViewToFront(); 986 } 987 // ZoomView is a special case to always be in the front. In L set to 988 // max elevation to make sure ZoomView is above other elevated views. 989 bringChildToFront(mZoomView); 990 if (ApiHelper.isLOrHigher()) { 991 setMaxElevation(mZoomView); 992 } 993 } 994 995 @TargetApi(Build.VERSION_CODES.LOLLIPOP) setMaxElevation(View v)996 private void setMaxElevation(View v) { 997 v.setElevation(Float.MAX_VALUE); 998 } 999 1000 /** 1001 * Returns the ID of the current item, or -1 if there is no data. 1002 */ getCurrentId()1003 private int getCurrentId() { 1004 ViewItem current = mViewItem[mCurrentItem]; 1005 if (current == null) { 1006 return -1; 1007 } 1008 return current.getId(); 1009 } 1010 1011 /** 1012 * Keep the current item in the center. This functions does not check if the 1013 * current item is null. 1014 */ snapInCenter()1015 private void snapInCenter() { 1016 final ViewItem currItem = mViewItem[mCurrentItem]; 1017 if (currItem == null) { 1018 return; 1019 } 1020 final int currentViewCenter = currItem.getCenterX(); 1021 if (mController.isScrolling() || mIsUserScrolling 1022 || isCurrentItemCentered()) { 1023 return; 1024 } 1025 1026 int snapInTime = (int) (SNAP_IN_CENTER_TIME_MS 1027 * ((float) Math.abs(mCenterX - currentViewCenter)) 1028 / mDrawArea.width()); 1029 mController.scrollToPosition(currentViewCenter, 1030 snapInTime, false); 1031 if (isViewTypeSticky(currItem) && !mController.isScaling() && mScale != FULL_SCREEN_SCALE) { 1032 // Now going to full screen camera 1033 mController.goToFullScreen(); 1034 } 1035 } 1036 1037 /** 1038 * Translates the {@link ViewItem} on the left of the current one to match 1039 * the full-screen layout. In full-screen, we show only one {@link ViewItem} 1040 * which occupies the whole screen. The other left ones are put on the left 1041 * side in full scales. Does nothing if there's no next item. 1042 * 1043 * @param currItem The item ID of the current one to be translated. 1044 * @param drawAreaWidth The width of the current draw area. 1045 * @param scaleFraction A {@code float} between 0 and 1. 0 if the current 1046 * scale is {@code FILM_STRIP_SCALE}. 1 if the current scale is 1047 * {@code FULL_SCREEN_SCALE}. 1048 */ translateLeftViewItem( int currItem, int drawAreaWidth, float scaleFraction)1049 private void translateLeftViewItem( 1050 int currItem, int drawAreaWidth, float scaleFraction) { 1051 if (currItem < 0 || currItem > BUFFER_SIZE - 1) { 1052 Log.e(TAG, "currItem id out of bound."); 1053 return; 1054 } 1055 1056 final ViewItem curr = mViewItem[currItem]; 1057 final ViewItem next = mViewItem[currItem + 1]; 1058 if (curr == null || next == null) { 1059 Log.e(TAG, "Invalid view item (curr or next == null). curr = " 1060 + currItem); 1061 return; 1062 } 1063 1064 final int currCenterX = curr.getCenterX(); 1065 final int nextCenterX = next.getCenterX(); 1066 final int translate = (int) ((nextCenterX - drawAreaWidth 1067 - currCenterX) * scaleFraction); 1068 1069 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale); 1070 curr.setAlpha(1f); 1071 curr.setVisibility(VISIBLE); 1072 1073 if (inFullScreen()) { 1074 curr.setTranslationX(translate * (mCenterX - currCenterX) / (nextCenterX - currCenterX)); 1075 } else { 1076 curr.setTranslationX(translate); 1077 } 1078 } 1079 1080 /** 1081 * Fade out the {@link ViewItem} on the right of the current one in 1082 * full-screen layout. Does nothing if there's no previous item. 1083 * 1084 * @param currItemId The ID of the item to fade. 1085 */ fadeAndScaleRightViewItem(int currItemId)1086 private void fadeAndScaleRightViewItem(int currItemId) { 1087 if (currItemId < 1 || currItemId > BUFFER_SIZE) { 1088 Log.e(TAG, "currItem id out of bound."); 1089 return; 1090 } 1091 1092 final ViewItem currItem = mViewItem[currItemId]; 1093 final ViewItem prevItem = mViewItem[currItemId - 1]; 1094 if (currItem == null || prevItem == null) { 1095 Log.e(TAG, "Invalid view item (curr or prev == null). curr = " 1096 + currItemId); 1097 return; 1098 } 1099 1100 if (currItemId > mCurrentItem + 1) { 1101 // Every item not right next to the mCurrentItem is invisible. 1102 currItem.setVisibility(INVISIBLE); 1103 return; 1104 } 1105 final int prevCenterX = prevItem.getCenterX(); 1106 if (mCenterX <= prevCenterX) { 1107 // Shortcut. If the position is at the center of the previous one, 1108 // set to invisible too. 1109 currItem.setVisibility(INVISIBLE); 1110 return; 1111 } 1112 final int currCenterX = currItem.getCenterX(); 1113 final float fadeDownFraction = 1114 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX); 1115 currItem.layoutWithTranslationX(mDrawArea, currCenterX, 1116 FILM_STRIP_SCALE + (1f - FILM_STRIP_SCALE) * fadeDownFraction); 1117 currItem.setAlpha(fadeDownFraction); 1118 currItem.setTranslationX(0); 1119 currItem.setVisibility(VISIBLE); 1120 } 1121 layoutViewItems(boolean layoutChanged)1122 private void layoutViewItems(boolean layoutChanged) { 1123 if (mViewItem[mCurrentItem] == null || 1124 mDrawArea.width() == 0 || 1125 mDrawArea.height() == 0) { 1126 return; 1127 } 1128 1129 // If the layout changed, we need to adjust the current position so 1130 // that if an item is centered before the change, it's still centered. 1131 if (layoutChanged) { 1132 mViewItem[mCurrentItem].setLeftPosition( 1133 mCenterX - mViewItem[mCurrentItem].getMeasuredWidth() / 2); 1134 } 1135 1136 if (inZoomView()) { 1137 return; 1138 } 1139 /** 1140 * Transformed scale fraction between 0 and 1. 0 if the scale is 1141 * {@link FILM_STRIP_SCALE}. 1 if the scale is {@link FULL_SCREEN_SCALE} 1142 * . 1143 */ 1144 final float scaleFraction = mViewAnimInterpolator.getInterpolation( 1145 (mScale - FILM_STRIP_SCALE) / (FULL_SCREEN_SCALE - FILM_STRIP_SCALE)); 1146 final int fullScreenWidth = mDrawArea.width() + mViewGapInPixel; 1147 1148 // Decide the position for all view items on the left and the right 1149 // first. 1150 1151 // Left items. 1152 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) { 1153 final ViewItem curr = mViewItem[itemID]; 1154 if (curr == null) { 1155 break; 1156 } 1157 1158 // First, layout relatively to the next one. 1159 final int currLeft = mViewItem[itemID + 1].getLeftPosition() 1160 - curr.getMeasuredWidth() - mViewGapInPixel; 1161 curr.setLeftPosition(currLeft); 1162 } 1163 // Right items. 1164 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) { 1165 final ViewItem curr = mViewItem[itemID]; 1166 if (curr == null) { 1167 break; 1168 } 1169 1170 // First, layout relatively to the previous one. 1171 final ViewItem prev = mViewItem[itemID - 1]; 1172 final int currLeft = 1173 prev.getLeftPosition() + prev.getMeasuredWidth() 1174 + mViewGapInPixel; 1175 curr.setLeftPosition(currLeft); 1176 } 1177 1178 // Special case for the one immediately on the right of the camera 1179 // preview. 1180 boolean immediateRight = 1181 (mViewItem[mCurrentItem].getId() == 1 && 1182 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY); 1183 1184 // Layout the current ViewItem first. 1185 if (immediateRight) { 1186 // Just do a simple layout without any special translation or 1187 // fading. The implementation in Gallery does not push the first 1188 // photo to the bottom of the camera preview. Simply place the 1189 // photo on the right of the preview. 1190 final ViewItem currItem = mViewItem[mCurrentItem]; 1191 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale); 1192 currItem.setTranslationX(0f); 1193 currItem.setAlpha(1f); 1194 } else if (scaleFraction == 1f) { 1195 final ViewItem currItem = mViewItem[mCurrentItem]; 1196 final int currCenterX = currItem.getCenterX(); 1197 if (mCenterX < currCenterX) { 1198 // In full-screen and mCenterX is on the left of the center, 1199 // we draw the current one to "fade down". 1200 fadeAndScaleRightViewItem(mCurrentItem); 1201 } else if (mCenterX > currCenterX) { 1202 // In full-screen and mCenterX is on the right of the center, 1203 // we draw the current one translated. 1204 translateLeftViewItem(mCurrentItem, fullScreenWidth, scaleFraction); 1205 } else { 1206 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale); 1207 currItem.setTranslationX(0f); 1208 currItem.setAlpha(1f); 1209 } 1210 } else { 1211 final ViewItem currItem = mViewItem[mCurrentItem]; 1212 // The normal filmstrip has no translation for the current item. If 1213 // it has translation before, gradually set it to zero. 1214 currItem.setTranslationX(currItem.getTranslationX() * scaleFraction); 1215 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale); 1216 if (mViewItem[mCurrentItem - 1] == null) { 1217 currItem.setAlpha(1f); 1218 } else { 1219 final int currCenterX = currItem.getCenterX(); 1220 final int prevCenterX = mViewItem[mCurrentItem - 1].getCenterX(); 1221 final float fadeDownFraction = 1222 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX); 1223 currItem.setAlpha( 1224 (1 - fadeDownFraction) * (1 - scaleFraction) + fadeDownFraction); 1225 } 1226 } 1227 1228 // Layout the rest dependent on the current scale. 1229 1230 // Items on the left 1231 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) { 1232 final ViewItem curr = mViewItem[itemID]; 1233 if (curr == null) { 1234 break; 1235 } 1236 translateLeftViewItem(itemID, fullScreenWidth, scaleFraction); 1237 } 1238 1239 // Items on the right 1240 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) { 1241 final ViewItem curr = mViewItem[itemID]; 1242 if (curr == null) { 1243 break; 1244 } 1245 1246 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale); 1247 if (curr.getId() == 1 && isViewTypeSticky(curr)) { 1248 // Special case for the one next to the camera preview. 1249 curr.setAlpha(1f); 1250 continue; 1251 } 1252 1253 if (scaleFraction == 1) { 1254 // It's in full-screen mode. 1255 fadeAndScaleRightViewItem(itemID); 1256 } else { 1257 boolean setToVisible = (curr.getVisibility() == INVISIBLE); 1258 1259 if (itemID == mCurrentItem + 1) { 1260 curr.setAlpha(1f - scaleFraction); 1261 } else { 1262 if (scaleFraction == 0f) { 1263 curr.setAlpha(1f); 1264 } else { 1265 setToVisible = false; 1266 } 1267 } 1268 1269 if (setToVisible) { 1270 curr.setVisibility(VISIBLE); 1271 } 1272 1273 curr.setTranslationX( 1274 (mViewItem[mCurrentItem].getLeftPosition() - curr.getLeftPosition()) * 1275 scaleFraction); 1276 } 1277 } 1278 1279 stepIfNeeded(); 1280 } 1281 isViewTypeSticky(ViewItem item)1282 private boolean isViewTypeSticky(ViewItem item) { 1283 if (item == null) { 1284 return false; 1285 } 1286 return mDataAdapter.getImageData(item.getId()).getViewType() == 1287 ImageData.VIEW_TYPE_STICKY; 1288 } 1289 1290 @Override onDraw(Canvas c)1291 public void onDraw(Canvas c) { 1292 // TODO: remove layoutViewItems() here. 1293 layoutViewItems(false); 1294 super.onDraw(c); 1295 } 1296 1297 @Override onLayout(boolean changed, int l, int t, int r, int b)1298 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1299 mDrawArea.left = 0; 1300 mDrawArea.top = 0; 1301 mDrawArea.right = r - l; 1302 mDrawArea.bottom = b - t; 1303 mZoomView.layout(mDrawArea.left, mDrawArea.top, mDrawArea.right, mDrawArea.bottom); 1304 // TODO: Need a more robust solution to decide when to re-layout 1305 // If in the middle of zooming, only re-layout when the layout has 1306 // changed. 1307 if (!inZoomView() || changed) { 1308 resetZoomView(); 1309 layoutViewItems(changed); 1310 } 1311 } 1312 1313 /** 1314 * Clears the translation and scale that has been set on the view, cancels 1315 * any loading request for image partial decoding, and hides zoom view. This 1316 * is needed for when there is a layout change (e.g. when users re-enter the 1317 * app, or rotate the device, etc). 1318 */ resetZoomView()1319 private void resetZoomView() { 1320 if (!inZoomView()) { 1321 return; 1322 } 1323 ViewItem current = mViewItem[mCurrentItem]; 1324 if (current == null) { 1325 return; 1326 } 1327 mScale = FULL_SCREEN_SCALE; 1328 mController.cancelZoomAnimation(); 1329 mController.cancelFlingAnimation(); 1330 current.resetTransform(); 1331 mController.cancelLoadingZoomedImage(); 1332 mZoomView.setVisibility(GONE); 1333 mController.setSurroundingViewsVisible(true); 1334 } 1335 hideZoomView()1336 private void hideZoomView() { 1337 if (inZoomView()) { 1338 mController.cancelLoadingZoomedImage(); 1339 mZoomView.setVisibility(GONE); 1340 } 1341 } 1342 slideViewBack(ViewItem item)1343 private void slideViewBack(ViewItem item) { 1344 item.animateTranslationX(0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator); 1345 item.animateTranslationY(0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator); 1346 item.animateAlpha(1f, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator); 1347 } 1348 animateItemRemoval(int dataID, final ImageData data)1349 private void animateItemRemoval(int dataID, final ImageData data) { 1350 if (mScale > FULL_SCREEN_SCALE) { 1351 resetZoomView(); 1352 } 1353 int removedItemId = findItemByDataID(dataID); 1354 1355 // adjust the data id to be consistent 1356 for (int i = 0; i < BUFFER_SIZE; i++) { 1357 if (mViewItem[i] == null || mViewItem[i].getId() <= dataID) { 1358 continue; 1359 } 1360 mViewItem[i].setId(mViewItem[i].getId() - 1); 1361 } 1362 if (removedItemId == -1) { 1363 return; 1364 } 1365 1366 final ViewItem removedItem = mViewItem[removedItemId]; 1367 final int offsetX = removedItem.getMeasuredWidth() + mViewGapInPixel; 1368 1369 for (int i = removedItemId + 1; i < BUFFER_SIZE; i++) { 1370 if (mViewItem[i] != null) { 1371 mViewItem[i].setLeftPosition(mViewItem[i].getLeftPosition() - offsetX); 1372 } 1373 } 1374 1375 if (removedItemId >= mCurrentItem 1376 && mViewItem[removedItemId].getId() < mDataAdapter.getTotalNumber()) { 1377 // Fill the removed item by left shift when the current one or 1378 // anyone on the right is removed, and there's more data on the 1379 // right available. 1380 for (int i = removedItemId; i < BUFFER_SIZE - 1; i++) { 1381 mViewItem[i] = mViewItem[i + 1]; 1382 } 1383 1384 // pull data out from the DataAdapter for the last one. 1385 int curr = BUFFER_SIZE - 1; 1386 int prev = curr - 1; 1387 if (mViewItem[prev] != null) { 1388 mViewItem[curr] = buildItemFromData(mViewItem[prev].getId() + 1); 1389 } 1390 1391 // The animation part. 1392 if (inFullScreen()) { 1393 mViewItem[mCurrentItem].setVisibility(VISIBLE); 1394 ViewItem nextItem = mViewItem[mCurrentItem + 1]; 1395 if (nextItem != null) { 1396 nextItem.setVisibility(INVISIBLE); 1397 } 1398 } 1399 1400 // Translate the views to their original places. 1401 for (int i = removedItemId; i < BUFFER_SIZE; i++) { 1402 if (mViewItem[i] != null) { 1403 mViewItem[i].setTranslationX(offsetX); 1404 } 1405 } 1406 1407 // The end of the filmstrip might have been changed. 1408 // The mCenterX might be out of the bound. 1409 ViewItem currItem = mViewItem[mCurrentItem]; 1410 if(currItem!=null) { 1411 if (currItem.getId() == mDataAdapter.getTotalNumber() - 1 1412 && mCenterX > currItem.getCenterX()) { 1413 int adjustDiff = currItem.getCenterX() - mCenterX; 1414 mCenterX = currItem.getCenterX(); 1415 for (int i = 0; i < BUFFER_SIZE; i++) { 1416 if (mViewItem[i] != null) { 1417 mViewItem[i].translateXScaledBy(adjustDiff); 1418 } 1419 } 1420 } 1421 } else { 1422 // CurrItem should NOT be NULL, but if is, at least don't crash. 1423 Log.w(TAG,"Caught invalid update in removal animation."); 1424 } 1425 } else { 1426 // fill the removed place by right shift 1427 mCenterX -= offsetX; 1428 1429 for (int i = removedItemId; i > 0; i--) { 1430 mViewItem[i] = mViewItem[i - 1]; 1431 } 1432 1433 // pull data out from the DataAdapter for the first one. 1434 int curr = 0; 1435 int next = curr + 1; 1436 if (mViewItem[next] != null) { 1437 mViewItem[curr] = buildItemFromData(mViewItem[next].getId() - 1); 1438 } 1439 1440 // Translate the views to their original places. 1441 for (int i = removedItemId; i >= 0; i--) { 1442 if (mViewItem[i] != null) { 1443 mViewItem[i].setTranslationX(-offsetX); 1444 } 1445 } 1446 } 1447 1448 int transY = getHeight() / 8; 1449 if (removedItem.getTranslationY() < 0) { 1450 transY = -transY; 1451 } 1452 removedItem.animateTranslationY(removedItem.getTranslationY() + transY, 1453 GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator); 1454 removedItem.animateAlpha(0f, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator); 1455 postDelayed(new Runnable() { 1456 @Override 1457 public void run() { 1458 removedItem.removeViewFromHierarchy(false); 1459 } 1460 }, GEOMETRY_ADJUST_TIME_MS); 1461 1462 adjustChildZOrder(); 1463 invalidate(); 1464 1465 // Now, slide every one back. 1466 if (mViewItem[mCurrentItem] == null) { 1467 return; 1468 } 1469 for (int i = 0; i < BUFFER_SIZE; i++) { 1470 if (mViewItem[i] != null 1471 && mViewItem[i].getTranslationX() != 0f) { 1472 slideViewBack(mViewItem[i]); 1473 } 1474 } 1475 if (isCurrentItemCentered() && isViewTypeSticky(mViewItem[mCurrentItem])) { 1476 // Special case for scrolling onto the camera preview after removal. 1477 mController.goToFullScreen(); 1478 } 1479 } 1480 1481 // returns -1 on failure. findItemByDataID(int dataID)1482 private int findItemByDataID(int dataID) { 1483 for (int i = 0; i < BUFFER_SIZE; i++) { 1484 if (mViewItem[i] != null 1485 && mViewItem[i].getId() == dataID) { 1486 return i; 1487 } 1488 } 1489 return -1; 1490 } 1491 updateInsertion(int dataID)1492 private void updateInsertion(int dataID) { 1493 int insertedItemId = findItemByDataID(dataID); 1494 if (insertedItemId == -1) { 1495 // Not in the current item buffers. Check if it's inserted 1496 // at the end. 1497 if (dataID == mDataAdapter.getTotalNumber() - 1) { 1498 int prev = findItemByDataID(dataID - 1); 1499 if (prev >= 0 && prev < BUFFER_SIZE - 1) { 1500 // The previous data is in the buffer and we still 1501 // have room for the inserted data. 1502 insertedItemId = prev + 1; 1503 } 1504 } 1505 } 1506 1507 // adjust the data id to be consistent 1508 for (int i = 0; i < BUFFER_SIZE; i++) { 1509 if (mViewItem[i] == null || mViewItem[i].getId() < dataID) { 1510 continue; 1511 } 1512 mViewItem[i].setId(mViewItem[i].getId() + 1); 1513 } 1514 if (insertedItemId == -1) { 1515 return; 1516 } 1517 1518 final ImageData data = mDataAdapter.getImageData(dataID); 1519 Point dim = CameraUtil 1520 .resizeToFill(data.getWidth(), data.getHeight(), data.getRotation(), 1521 getMeasuredWidth(), getMeasuredHeight()); 1522 final int offsetX = dim.x + mViewGapInPixel; 1523 ViewItem viewItem = buildItemFromData(dataID); 1524 if (viewItem == null) { 1525 Log.w(TAG, "unable to build inserted item from data"); 1526 return; 1527 } 1528 1529 if (insertedItemId >= mCurrentItem) { 1530 if (insertedItemId == mCurrentItem) { 1531 viewItem.setLeftPosition(mViewItem[mCurrentItem].getLeftPosition()); 1532 } 1533 // Shift right to make rooms for newly inserted item. 1534 removeItem(BUFFER_SIZE - 1); 1535 for (int i = BUFFER_SIZE - 1; i > insertedItemId; i--) { 1536 mViewItem[i] = mViewItem[i - 1]; 1537 if (mViewItem[i] != null) { 1538 mViewItem[i].setTranslationX(-offsetX); 1539 slideViewBack(mViewItem[i]); 1540 } 1541 } 1542 } else { 1543 // Shift left. Put the inserted data on the left instead of the 1544 // found position. 1545 --insertedItemId; 1546 if (insertedItemId < 0) { 1547 return; 1548 } 1549 removeItem(0); 1550 for (int i = 1; i <= insertedItemId; i++) { 1551 if (mViewItem[i] != null) { 1552 mViewItem[i].setTranslationX(offsetX); 1553 slideViewBack(mViewItem[i]); 1554 mViewItem[i - 1] = mViewItem[i]; 1555 } 1556 } 1557 } 1558 1559 mViewItem[insertedItemId] = viewItem; 1560 viewItem.setAlpha(0f); 1561 viewItem.setTranslationY(getHeight() / 8); 1562 slideViewBack(viewItem); 1563 adjustChildZOrder(); 1564 invalidate(); 1565 } 1566 setDataAdapter(DataAdapter adapter)1567 private void setDataAdapter(DataAdapter adapter) { 1568 mDataAdapter = adapter; 1569 int maxEdge = (int) (Math.max(this.getHeight(), this.getWidth()) 1570 * FILM_STRIP_SCALE); 1571 mDataAdapter.suggestViewSizeBound(maxEdge, maxEdge); 1572 mDataAdapter.setListener(new DataAdapter.Listener() { 1573 @Override 1574 public void onDataLoaded() { 1575 reload(); 1576 } 1577 1578 @Override 1579 public void onDataUpdated(DataAdapter.UpdateReporter reporter) { 1580 update(reporter); 1581 } 1582 1583 @Override 1584 public void onDataInserted(int dataId, ImageData data) { 1585 if (mViewItem[mCurrentItem] == null) { 1586 // empty now, simply do a reload. 1587 reload(); 1588 } else { 1589 updateInsertion(dataId); 1590 } 1591 if (mListener != null) { 1592 mListener.onDataFocusChanged(dataId, getCurrentId()); 1593 } 1594 } 1595 1596 @Override 1597 public void onDataRemoved(int dataId, ImageData data) { 1598 animateItemRemoval(dataId, data); 1599 if (mListener != null) { 1600 mListener.onDataFocusChanged(dataId, getCurrentId()); 1601 } 1602 } 1603 }); 1604 } 1605 inFilmstrip()1606 private boolean inFilmstrip() { 1607 return (mScale == FILM_STRIP_SCALE); 1608 } 1609 inFullScreen()1610 private boolean inFullScreen() { 1611 return (mScale == FULL_SCREEN_SCALE); 1612 } 1613 inZoomView()1614 private boolean inZoomView() { 1615 return (mScale > FULL_SCREEN_SCALE); 1616 } 1617 isCameraPreview()1618 private boolean isCameraPreview() { 1619 return isViewTypeSticky(mViewItem[mCurrentItem]); 1620 } 1621 inCameraFullscreen()1622 private boolean inCameraFullscreen() { 1623 return isDataAtCenter(0) && inFullScreen() 1624 && (isViewTypeSticky(mViewItem[mCurrentItem])); 1625 } 1626 1627 @Override onInterceptTouchEvent(MotionEvent ev)1628 public boolean onInterceptTouchEvent(MotionEvent ev) { 1629 if (mController.isScrolling()) { 1630 return true; 1631 } 1632 1633 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 1634 mCheckToIntercept = true; 1635 mDown = MotionEvent.obtain(ev); 1636 ViewItem viewItem = mViewItem[mCurrentItem]; 1637 // Do not intercept touch if swipe is not enabled 1638 if (viewItem != null && !mDataAdapter.canSwipeInFullScreen(viewItem.getId())) { 1639 mCheckToIntercept = false; 1640 } 1641 return false; 1642 } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { 1643 // Do not intercept touch once child is in zoom mode 1644 mCheckToIntercept = false; 1645 return false; 1646 } else { 1647 if (!mCheckToIntercept) { 1648 return false; 1649 } 1650 if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) { 1651 return false; 1652 } 1653 int deltaX = (int) (ev.getX() - mDown.getX()); 1654 int deltaY = (int) (ev.getY() - mDown.getY()); 1655 if (ev.getActionMasked() == MotionEvent.ACTION_MOVE 1656 && deltaX < mSlop * (-1)) { 1657 // intercept left swipe 1658 if (Math.abs(deltaX) >= Math.abs(deltaY) * 2) { 1659 return true; 1660 } 1661 } 1662 } 1663 return false; 1664 } 1665 1666 @Override onTouchEvent(MotionEvent ev)1667 public boolean onTouchEvent(MotionEvent ev) { 1668 return mGestureRecognizer.onTouchEvent(ev); 1669 } 1670 getGestureListener()1671 FilmstripGestureRecognizer.Listener getGestureListener() { 1672 return mGestureListener; 1673 } 1674 updateViewItem(int itemID)1675 private void updateViewItem(int itemID) { 1676 ViewItem item = mViewItem[itemID]; 1677 if (item == null) { 1678 Log.e(TAG, "trying to update an null item"); 1679 return; 1680 } 1681 item.removeViewFromHierarchy(true); 1682 1683 ViewItem newItem = buildItemFromData(item.getId()); 1684 if (newItem == null) { 1685 Log.e(TAG, "new item is null"); 1686 // keep using the old data. 1687 item.addViewToHierarchy(); 1688 return; 1689 } 1690 newItem.copyAttributes(item); 1691 mViewItem[itemID] = newItem; 1692 mZoomView.resetDecoder(); 1693 1694 boolean stopScroll = clampCenterX(); 1695 if (stopScroll) { 1696 mController.stopScrolling(true); 1697 } 1698 adjustChildZOrder(); 1699 invalidate(); 1700 if (mListener != null) { 1701 mListener.onDataUpdated(newItem.getId()); 1702 } 1703 } 1704 1705 /** Some of the data is changed. */ update(DataAdapter.UpdateReporter reporter)1706 private void update(DataAdapter.UpdateReporter reporter) { 1707 // No data yet. 1708 if (mViewItem[mCurrentItem] == null) { 1709 reload(); 1710 return; 1711 } 1712 1713 // Check the current one. 1714 ViewItem curr = mViewItem[mCurrentItem]; 1715 int dataId = curr.getId(); 1716 if (reporter.isDataRemoved(dataId)) { 1717 reload(); 1718 return; 1719 } 1720 if (reporter.isDataUpdated(dataId)) { 1721 updateViewItem(mCurrentItem); 1722 final ImageData data = mDataAdapter.getImageData(dataId); 1723 if (!mIsUserScrolling && !mController.isScrolling()) { 1724 // If there is no scrolling at all, adjust mCenterX to place 1725 // the current item at the center. 1726 Point dim = CameraUtil.resizeToFill(data.getWidth(), data.getHeight(), 1727 data.getRotation(), getMeasuredWidth(), getMeasuredHeight()); 1728 mCenterX = curr.getLeftPosition() + dim.x / 2; 1729 } 1730 } 1731 1732 // Check left 1733 for (int i = mCurrentItem - 1; i >= 0; i--) { 1734 curr = mViewItem[i]; 1735 if (curr != null) { 1736 dataId = curr.getId(); 1737 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) { 1738 updateViewItem(i); 1739 } 1740 } else { 1741 ViewItem next = mViewItem[i + 1]; 1742 if (next != null) { 1743 mViewItem[i] = buildItemFromData(next.getId() - 1); 1744 } 1745 } 1746 } 1747 1748 // Check right 1749 for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) { 1750 curr = mViewItem[i]; 1751 if (curr != null) { 1752 dataId = curr.getId(); 1753 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) { 1754 updateViewItem(i); 1755 } 1756 } else { 1757 ViewItem prev = mViewItem[i - 1]; 1758 if (prev != null) { 1759 mViewItem[i] = buildItemFromData(prev.getId() + 1); 1760 } 1761 } 1762 } 1763 adjustChildZOrder(); 1764 // Request a layout to find the measured width/height of the view first. 1765 requestLayout(); 1766 // Update photo sphere visibility after metadata fully written. 1767 } 1768 1769 /** 1770 * The whole data might be totally different. Flush all and load from the 1771 * start. Filmstrip will be centered on the first item, i.e. the camera 1772 * preview. 1773 */ reload()1774 private void reload() { 1775 mController.stopScrolling(true); 1776 mController.stopScale(); 1777 mDataIdOnUserScrolling = 0; 1778 1779 int prevId = -1; 1780 if (mViewItem[mCurrentItem] != null) { 1781 prevId = mViewItem[mCurrentItem].getId(); 1782 } 1783 1784 // Remove all views from the mViewItem buffer, except the camera view. 1785 for (int i = 0; i < mViewItem.length; i++) { 1786 if (mViewItem[i] == null) { 1787 continue; 1788 } 1789 mViewItem[i].removeViewFromHierarchy(false); 1790 } 1791 1792 // Clear out the mViewItems and rebuild with camera in the center. 1793 Arrays.fill(mViewItem, null); 1794 int dataNumber = mDataAdapter.getTotalNumber(); 1795 if (dataNumber == 0) { 1796 return; 1797 } 1798 1799 mViewItem[mCurrentItem] = buildItemFromData(0); 1800 if (mViewItem[mCurrentItem] == null) { 1801 return; 1802 } 1803 mViewItem[mCurrentItem].setLeftPosition(0); 1804 for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) { 1805 mViewItem[i] = buildItemFromData(mViewItem[i - 1].getId() + 1); 1806 if (mViewItem[i] == null) { 1807 break; 1808 } 1809 } 1810 1811 // Ensure that the views in mViewItem will layout the first in the 1812 // center of the display upon a reload. 1813 mCenterX = -1; 1814 mScale = FILM_STRIP_SCALE; 1815 1816 adjustChildZOrder(); 1817 invalidate(); 1818 1819 if (mListener != null) { 1820 mListener.onDataReloaded(); 1821 mListener.onDataFocusChanged(prevId, mViewItem[mCurrentItem].getId()); 1822 } 1823 } 1824 promoteData(int itemID, int dataID)1825 private void promoteData(int itemID, int dataID) { 1826 if (mListener != null) { 1827 mListener.onFocusedDataPromoted(dataID); 1828 } 1829 } 1830 demoteData(int itemID, int dataID)1831 private void demoteData(int itemID, int dataID) { 1832 if (mListener != null) { 1833 mListener.onFocusedDataDemoted(dataID); 1834 } 1835 } 1836 onEnterFilmstrip()1837 private void onEnterFilmstrip() { 1838 if (mListener != null) { 1839 mListener.onEnterFilmstrip(getCurrentId()); 1840 } 1841 } 1842 onLeaveFilmstrip()1843 private void onLeaveFilmstrip() { 1844 if (mListener != null) { 1845 mListener.onLeaveFilmstrip(getCurrentId()); 1846 } 1847 } 1848 onEnterFullScreen()1849 private void onEnterFullScreen() { 1850 mFullScreenUIHidden = false; 1851 if (mListener != null) { 1852 mListener.onEnterFullScreenUiShown(getCurrentId()); 1853 } 1854 } 1855 onLeaveFullScreen()1856 private void onLeaveFullScreen() { 1857 if (mListener != null) { 1858 mListener.onLeaveFullScreenUiShown(getCurrentId()); 1859 } 1860 } 1861 onEnterFullScreenUiHidden()1862 private void onEnterFullScreenUiHidden() { 1863 mFullScreenUIHidden = true; 1864 if (mListener != null) { 1865 mListener.onEnterFullScreenUiHidden(getCurrentId()); 1866 } 1867 } 1868 onLeaveFullScreenUiHidden()1869 private void onLeaveFullScreenUiHidden() { 1870 mFullScreenUIHidden = false; 1871 if (mListener != null) { 1872 mListener.onLeaveFullScreenUiHidden(getCurrentId()); 1873 } 1874 } 1875 onEnterZoomView()1876 private void onEnterZoomView() { 1877 if (mListener != null) { 1878 mListener.onEnterZoomView(getCurrentId()); 1879 } 1880 } 1881 onLeaveZoomView()1882 private void onLeaveZoomView() { 1883 mController.setSurroundingViewsVisible(true); 1884 } 1885 1886 /** 1887 * MyController controls all the geometry animations. It passively tells the 1888 * geometry information on demand. 1889 */ 1890 private class MyController implements FilmstripController { 1891 1892 private final ValueAnimator mScaleAnimator; 1893 private ValueAnimator mZoomAnimator; 1894 private AnimatorSet mFlingAnimator; 1895 1896 private final MyScroller mScroller; 1897 private boolean mCanStopScroll; 1898 1899 private final MyScroller.Listener mScrollerListener = 1900 new MyScroller.Listener() { 1901 @Override 1902 public void onScrollUpdate(int currX, int currY) { 1903 mCenterX = currX; 1904 1905 boolean stopScroll = clampCenterX(); 1906 if (stopScroll) { 1907 mController.stopScrolling(true); 1908 } 1909 invalidate(); 1910 } 1911 1912 @Override 1913 public void onScrollEnd() { 1914 mCanStopScroll = true; 1915 if (mViewItem[mCurrentItem] == null) { 1916 return; 1917 } 1918 snapInCenter(); 1919 if (isCurrentItemCentered() 1920 && isViewTypeSticky(mViewItem[mCurrentItem])) { 1921 // Special case for the scrolling end on the camera 1922 // preview. 1923 goToFullScreen(); 1924 } 1925 } 1926 }; 1927 1928 private final ValueAnimator.AnimatorUpdateListener mScaleAnimatorUpdateListener = 1929 new ValueAnimator.AnimatorUpdateListener() { 1930 @Override 1931 public void onAnimationUpdate(ValueAnimator animation) { 1932 if (mViewItem[mCurrentItem] == null) { 1933 return; 1934 } 1935 mScale = (Float) animation.getAnimatedValue(); 1936 invalidate(); 1937 } 1938 }; 1939 MyController(Context context)1940 MyController(Context context) { 1941 TimeInterpolator decelerateInterpolator = new DecelerateInterpolator(1.5f); 1942 mScroller = new MyScroller(mActivity.getAndroidContext(), 1943 new Handler(mActivity.getMainLooper()), 1944 mScrollerListener, decelerateInterpolator); 1945 mCanStopScroll = true; 1946 1947 mScaleAnimator = new ValueAnimator(); 1948 mScaleAnimator.addUpdateListener(mScaleAnimatorUpdateListener); 1949 mScaleAnimator.setInterpolator(decelerateInterpolator); 1950 mScaleAnimator.addListener(new Animator.AnimatorListener() { 1951 @Override 1952 public void onAnimationStart(Animator animator) { 1953 if (mScale == FULL_SCREEN_SCALE) { 1954 onLeaveFullScreen(); 1955 } else { 1956 if (mScale == FILM_STRIP_SCALE) { 1957 onLeaveFilmstrip(); 1958 } 1959 } 1960 } 1961 1962 @Override 1963 public void onAnimationEnd(Animator animator) { 1964 if (mScale == FULL_SCREEN_SCALE) { 1965 onEnterFullScreen(); 1966 } else { 1967 if (mScale == FILM_STRIP_SCALE) { 1968 onEnterFilmstrip(); 1969 } 1970 } 1971 zoomAtIndexChanged(); 1972 } 1973 1974 @Override 1975 public void onAnimationCancel(Animator animator) { 1976 1977 } 1978 1979 @Override 1980 public void onAnimationRepeat(Animator animator) { 1981 1982 } 1983 }); 1984 } 1985 1986 @Override setImageGap(int imageGap)1987 public void setImageGap(int imageGap) { 1988 FilmstripView.this.setViewGap(imageGap); 1989 } 1990 1991 @Override getCurrentId()1992 public int getCurrentId() { 1993 return FilmstripView.this.getCurrentId(); 1994 } 1995 1996 @Override setDataAdapter(DataAdapter adapter)1997 public void setDataAdapter(DataAdapter adapter) { 1998 FilmstripView.this.setDataAdapter(adapter); 1999 } 2000 2001 @Override inFilmstrip()2002 public boolean inFilmstrip() { 2003 return FilmstripView.this.inFilmstrip(); 2004 } 2005 2006 @Override inFullScreen()2007 public boolean inFullScreen() { 2008 return FilmstripView.this.inFullScreen(); 2009 } 2010 2011 @Override isCameraPreview()2012 public boolean isCameraPreview() { 2013 return FilmstripView.this.isCameraPreview(); 2014 } 2015 2016 @Override inCameraFullscreen()2017 public boolean inCameraFullscreen() { 2018 return FilmstripView.this.inCameraFullscreen(); 2019 } 2020 2021 @Override setListener(FilmstripListener l)2022 public void setListener(FilmstripListener l) { 2023 FilmstripView.this.setListener(l); 2024 } 2025 2026 @Override isScrolling()2027 public boolean isScrolling() { 2028 return !mScroller.isFinished(); 2029 } 2030 2031 @Override isScaling()2032 public boolean isScaling() { 2033 return mScaleAnimator.isRunning(); 2034 } 2035 estimateMinX(int dataID, int leftPos, int viewWidth)2036 private int estimateMinX(int dataID, int leftPos, int viewWidth) { 2037 return leftPos - (dataID + 100) * (viewWidth + mViewGapInPixel); 2038 } 2039 estimateMaxX(int dataID, int leftPos, int viewWidth)2040 private int estimateMaxX(int dataID, int leftPos, int viewWidth) { 2041 return leftPos 2042 + (mDataAdapter.getTotalNumber() - dataID + 100) 2043 * (viewWidth + mViewGapInPixel); 2044 } 2045 2046 /** Zoom all the way in or out on the image at the given pivot point. */ zoomAt(final ViewItem current, final float focusX, final float focusY)2047 private void zoomAt(final ViewItem current, final float focusX, final float focusY) { 2048 // End previous zoom animation, if any 2049 if (mZoomAnimator != null) { 2050 mZoomAnimator.end(); 2051 } 2052 // Calculate end scale 2053 final float maxScale = getCurrentDataMaxScale(false); 2054 final float endScale = mScale < maxScale - maxScale * TOLERANCE 2055 ? maxScale : FULL_SCREEN_SCALE; 2056 2057 mZoomAnimator = new ValueAnimator(); 2058 mZoomAnimator.setFloatValues(mScale, endScale); 2059 mZoomAnimator.setDuration(ZOOM_ANIMATION_DURATION_MS); 2060 mZoomAnimator.addListener(new Animator.AnimatorListener() { 2061 @Override 2062 public void onAnimationStart(Animator animation) { 2063 if (mScale == FULL_SCREEN_SCALE) { 2064 if (mFullScreenUIHidden) { 2065 onLeaveFullScreenUiHidden(); 2066 } else { 2067 onLeaveFullScreen(); 2068 } 2069 setSurroundingViewsVisible(false); 2070 } else if (inZoomView()) { 2071 onLeaveZoomView(); 2072 } 2073 cancelLoadingZoomedImage(); 2074 } 2075 2076 @Override 2077 public void onAnimationEnd(Animator animation) { 2078 // Make sure animation ends up having the correct scale even 2079 // if it is cancelled before it finishes 2080 if (mScale != endScale) { 2081 current.postScale(focusX, focusY, endScale / mScale, mDrawArea.width(), 2082 mDrawArea.height()); 2083 mScale = endScale; 2084 } 2085 2086 if (inFullScreen()) { 2087 setSurroundingViewsVisible(true); 2088 mZoomView.setVisibility(GONE); 2089 current.resetTransform(); 2090 onEnterFullScreenUiHidden(); 2091 } else { 2092 mController.loadZoomedImage(); 2093 onEnterZoomView(); 2094 } 2095 mZoomAnimator = null; 2096 zoomAtIndexChanged(); 2097 } 2098 2099 @Override 2100 public void onAnimationCancel(Animator animation) { 2101 // Do nothing. 2102 } 2103 2104 @Override 2105 public void onAnimationRepeat(Animator animation) { 2106 // Do nothing. 2107 } 2108 }); 2109 2110 mZoomAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 2111 @Override 2112 public void onAnimationUpdate(ValueAnimator animation) { 2113 float newScale = (Float) animation.getAnimatedValue(); 2114 float postScale = newScale / mScale; 2115 mScale = newScale; 2116 current.postScale(focusX, focusY, postScale, mDrawArea.width(), 2117 mDrawArea.height()); 2118 } 2119 }); 2120 mZoomAnimator.start(); 2121 } 2122 2123 @Override 2124 public void scroll(float deltaX) { 2125 if (!stopScrolling(false)) { 2126 return; 2127 } 2128 mCenterX += deltaX; 2129 2130 boolean stopScroll = clampCenterX(); 2131 if (stopScroll) { 2132 mController.stopScrolling(true); 2133 } 2134 invalidate(); 2135 } 2136 2137 @Override 2138 public void fling(float velocityX) { 2139 if (!stopScrolling(false)) { 2140 return; 2141 } 2142 final ViewItem item = mViewItem[mCurrentItem]; 2143 if (item == null) { 2144 return; 2145 } 2146 2147 float scaledVelocityX = velocityX / mScale; 2148 if (inFullScreen() && isViewTypeSticky(item) && scaledVelocityX < 0) { 2149 // Swipe left in camera preview. 2150 goToFilmstrip(); 2151 } 2152 2153 int w = getWidth(); 2154 // Estimation of possible length on the left. To ensure the 2155 // velocity doesn't become too slow eventually, we add a huge number 2156 // to the estimated maximum. 2157 int minX = estimateMinX(item.getId(), item.getLeftPosition(), w); 2158 // Estimation of possible length on the right. Likewise, exaggerate 2159 // the possible maximum too. 2160 int maxX = estimateMaxX(item.getId(), item.getLeftPosition(), w); 2161 mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0); 2162 } 2163 2164 void flingInsideZoomView(float velocityX, float velocityY) { 2165 if (!inZoomView()) { 2166 return; 2167 } 2168 2169 final ViewItem current = mViewItem[mCurrentItem]; 2170 if (current == null) { 2171 return; 2172 } 2173 2174 final int factor = DECELERATION_FACTOR; 2175 // Deceleration curve for distance: 2176 // S(t) = s + (e - s) * (1 - (1 - t/T) ^ factor) 2177 // Need to find the ending distance (e), so that the starting 2178 // velocity is the velocity of fling. 2179 // Velocity is the derivative of distance 2180 // V(t) = (e - s) * factor * (-1) * (1 - t/T) ^ (factor - 1) * (-1/T) 2181 // = (e - s) * factor * (1 - t/T) ^ (factor - 1) / T 2182 // Since V(0) = V0, we have e = T / factor * V0 + s 2183 2184 // Duration T should be long enough so that at the end of the fling, 2185 // image moves at 1 pixel/s for about P = 50ms = 0.05s 2186 // i.e. V(T - P) = 1 2187 // V(T - P) = V0 * (1 - (T -P) /T) ^ (factor - 1) = 1 2188 // T = P * V0 ^ (1 / (factor -1)) 2189 2190 final float velocity = Math.max(Math.abs(velocityX), Math.abs(velocityY)); 2191 // Dynamically calculate duration 2192 final float duration = (float) (FLING_COASTING_DURATION_S 2193 * Math.pow(velocity, (1f / (factor - 1f)))); 2194 2195 final float translationX = current.getTranslationX() * mScale; 2196 final float translationY = current.getTranslationY() * mScale; 2197 2198 final ValueAnimator decelerationX = ValueAnimator.ofFloat(translationX, 2199 translationX + duration / factor * velocityX); 2200 final ValueAnimator decelerationY = ValueAnimator.ofFloat(translationY, 2201 translationY + duration / factor * velocityY); 2202 2203 decelerationY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 2204 @Override 2205 public void onAnimationUpdate(ValueAnimator animation) { 2206 float transX = (Float) decelerationX.getAnimatedValue(); 2207 float transY = (Float) decelerationY.getAnimatedValue(); 2208 2209 current.updateTransform(transX, transY, mScale, 2210 mScale, mDrawArea.width(), mDrawArea.height()); 2211 } 2212 }); 2213 2214 mFlingAnimator = new AnimatorSet(); 2215 mFlingAnimator.play(decelerationX).with(decelerationY); 2216 mFlingAnimator.setDuration((int) (duration * 1000)); 2217 mFlingAnimator.setInterpolator(new TimeInterpolator() { 2218 @Override 2219 public float getInterpolation(float input) { 2220 return (float) (1.0f - Math.pow((1.0f - input), factor)); 2221 } 2222 }); 2223 mFlingAnimator.addListener(new Animator.AnimatorListener() { 2224 private boolean mCancelled = false; 2225 2226 @Override 2227 public void onAnimationStart(Animator animation) { 2228 2229 } 2230 2231 @Override 2232 public void onAnimationEnd(Animator animation) { 2233 if (!mCancelled) { 2234 loadZoomedImage(); 2235 } 2236 mFlingAnimator = null; 2237 } 2238 2239 @Override 2240 public void onAnimationCancel(Animator animation) { 2241 mCancelled = true; 2242 } 2243 2244 @Override 2245 public void onAnimationRepeat(Animator animation) { 2246 2247 } 2248 }); 2249 mFlingAnimator.start(); 2250 } 2251 2252 @Override 2253 public boolean stopScrolling(boolean forced) { 2254 if (!isScrolling()) { 2255 return true; 2256 } else if (!mCanStopScroll && !forced) { 2257 return false; 2258 } 2259 mScroller.forceFinished(true); 2260 return true; 2261 } 2262 2263 private void stopScale() { 2264 mScaleAnimator.cancel(); 2265 } 2266 2267 @Override 2268 public void scrollToPosition(int position, int duration, boolean interruptible) { 2269 if (mViewItem[mCurrentItem] == null) { 2270 return; 2271 } 2272 mCanStopScroll = interruptible; 2273 mScroller.startScroll(mCenterX, 0, position - mCenterX, 0, duration); 2274 } 2275 2276 @Override 2277 public boolean goToNextItem() { 2278 return goToItem(mCurrentItem + 1); 2279 } 2280 2281 @Override 2282 public boolean goToPreviousItem() { 2283 return goToItem(mCurrentItem - 1); 2284 } 2285 2286 private boolean goToItem(int itemIndex) { 2287 final ViewItem nextItem = mViewItem[itemIndex]; 2288 if (nextItem == null) { 2289 return false; 2290 } 2291 stopScrolling(true); 2292 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS * 2, false); 2293 2294 if (isViewTypeSticky(mViewItem[mCurrentItem])) { 2295 // Special case when moving from camera preview. 2296 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS); 2297 } 2298 return true; 2299 } 2300 2301 private void scaleTo(float scale, int duration) { 2302 if (mViewItem[mCurrentItem] == null) { 2303 return; 2304 } 2305 stopScale(); 2306 mScaleAnimator.setDuration(duration); 2307 mScaleAnimator.setFloatValues(mScale, scale); 2308 mScaleAnimator.start(); 2309 } 2310 2311 @Override 2312 public void goToFilmstrip() { 2313 if (mViewItem[mCurrentItem] == null) { 2314 return; 2315 } 2316 if (mScale == FILM_STRIP_SCALE) { 2317 return; 2318 } 2319 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS); 2320 2321 final ViewItem currItem = mViewItem[mCurrentItem]; 2322 final ViewItem nextItem = mViewItem[mCurrentItem + 1]; 2323 if (currItem.getId() == 0 && isViewTypeSticky(currItem) && nextItem != null) { 2324 // Deal with the special case of swiping in camera preview. 2325 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, false); 2326 } 2327 2328 if (mScale == FILM_STRIP_SCALE) { 2329 onLeaveFilmstrip(); 2330 } 2331 } 2332 2333 @Override 2334 public void goToFullScreen() { 2335 if (inFullScreen()) { 2336 return; 2337 } 2338 2339 scaleTo(FULL_SCREEN_SCALE, GEOMETRY_ADJUST_TIME_MS); 2340 } 2341 2342 private void cancelFlingAnimation() { 2343 // Cancels flinging for zoomed images 2344 if (isFlingAnimationRunning()) { 2345 mFlingAnimator.cancel(); 2346 } 2347 } 2348 2349 private void cancelZoomAnimation() { 2350 if (isZoomAnimationRunning()) { 2351 mZoomAnimator.cancel(); 2352 } 2353 } 2354 2355 private void setSurroundingViewsVisible(boolean visible) { 2356 // Hide everything on the left 2357 // TODO: Need to find a better way to toggle the visibility of views 2358 // around the current view. 2359 for (int i = 0; i < mCurrentItem; i++) { 2360 if (i == mCurrentItem || mViewItem[i] == null) { 2361 continue; 2362 } 2363 mViewItem[i].setVisibility(visible ? VISIBLE : INVISIBLE); 2364 } 2365 } 2366 2367 private Uri getCurrentUri() { 2368 ViewItem curr = mViewItem[mCurrentItem]; 2369 if (curr == null) { 2370 return Uri.EMPTY; 2371 } 2372 return mDataAdapter.getImageData(curr.getId()).getUri(); 2373 } 2374 2375 /** 2376 * Here we only support up to 1:1 image zoom (i.e. a 100% view of the 2377 * actual pixels). The max scale that we can apply on the view should 2378 * make the view same size as the image, in pixels. 2379 */ 2380 private float getCurrentDataMaxScale(boolean allowOverScale) { 2381 ViewItem curr = mViewItem[mCurrentItem]; 2382 if (curr == null) { 2383 return FULL_SCREEN_SCALE; 2384 } 2385 ImageData imageData = mDataAdapter.getImageData(curr.getId()); 2386 if (imageData == null || !imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) { 2387 return FULL_SCREEN_SCALE; 2388 } 2389 float imageWidth = imageData.getWidth(); 2390 if (imageData.getRotation() == 90 2391 || imageData.getRotation() == 270) { 2392 imageWidth = imageData.getHeight(); 2393 } 2394 float scale = imageWidth / curr.getWidth(); 2395 if (allowOverScale) { 2396 // In addition to the scale we apply to the view for 100% view 2397 // (i.e. each pixel on screen corresponds to a pixel in image) 2398 // we allow scaling beyond that for better detail viewing. 2399 scale *= mOverScaleFactor; 2400 } 2401 return scale; 2402 } 2403 2404 private void loadZoomedImage() { 2405 if (!inZoomView()) { 2406 return; 2407 } 2408 ViewItem curr = mViewItem[mCurrentItem]; 2409 if (curr == null) { 2410 return; 2411 } 2412 ImageData imageData = mDataAdapter.getImageData(curr.getId()); 2413 if (!imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) { 2414 return; 2415 } 2416 Uri uri = getCurrentUri(); 2417 RectF viewRect = curr.getViewRect(); 2418 if (uri == null || uri == Uri.EMPTY) { 2419 return; 2420 } 2421 int orientation = imageData.getRotation(); 2422 mZoomView.loadBitmap(uri, orientation, viewRect); 2423 } 2424 2425 private void cancelLoadingZoomedImage() { 2426 mZoomView.cancelPartialDecodingTask(); 2427 } 2428 2429 @Override 2430 public void goToFirstItem() { 2431 if (mViewItem[mCurrentItem] == null) { 2432 return; 2433 } 2434 resetZoomView(); 2435 // TODO: animate to camera if it is still in the mViewItem buffer 2436 // versus a full reload which will perform an immediate transition 2437 reload(); 2438 } 2439 2440 public boolean inZoomView() { 2441 return FilmstripView.this.inZoomView(); 2442 } 2443 2444 public boolean isFlingAnimationRunning() { 2445 return mFlingAnimator != null && mFlingAnimator.isRunning(); 2446 } 2447 2448 public boolean isZoomAnimationRunning() { 2449 return mZoomAnimator != null && mZoomAnimator.isRunning(); 2450 } 2451 } 2452 2453 private boolean isCurrentItemCentered() { 2454 return mViewItem[mCurrentItem].getCenterX() == mCenterX; 2455 } 2456 2457 private static class MyScroller { 2458 public interface Listener { 2459 public void onScrollUpdate(int currX, int currY); 2460 2461 public void onScrollEnd(); 2462 } 2463 2464 private final Handler mHandler; 2465 private final Listener mListener; 2466 2467 private final Scroller mScroller; 2468 2469 private final ValueAnimator mXScrollAnimator; 2470 private final Runnable mScrollChecker = new Runnable() { 2471 @Override 2472 public void run() { 2473 boolean newPosition = mScroller.computeScrollOffset(); 2474 if (!newPosition) { 2475 mListener.onScrollEnd(); 2476 return; 2477 } 2478 mListener.onScrollUpdate(mScroller.getCurrX(), mScroller.getCurrY()); 2479 mHandler.removeCallbacks(this); 2480 mHandler.post(this); 2481 } 2482 }; 2483 2484 private final ValueAnimator.AnimatorUpdateListener mXScrollAnimatorUpdateListener = 2485 new ValueAnimator.AnimatorUpdateListener() { 2486 @Override 2487 public void onAnimationUpdate(ValueAnimator animation) { 2488 mListener.onScrollUpdate((Integer) animation.getAnimatedValue(), 0); 2489 } 2490 }; 2491 2492 private final Animator.AnimatorListener mXScrollAnimatorListener = 2493 new Animator.AnimatorListener() { 2494 @Override 2495 public void onAnimationCancel(Animator animation) { 2496 // Do nothing. 2497 } 2498 2499 @Override 2500 public void onAnimationEnd(Animator animation) { 2501 mListener.onScrollEnd(); 2502 } 2503 2504 @Override 2505 public void onAnimationRepeat(Animator animation) { 2506 // Do nothing. 2507 } 2508 2509 @Override 2510 public void onAnimationStart(Animator animation) { 2511 // Do nothing. 2512 } 2513 }; 2514 2515 public MyScroller(Context ctx, Handler handler, Listener listener, 2516 TimeInterpolator interpolator) { 2517 mHandler = handler; 2518 mListener = listener; 2519 mScroller = new Scroller(ctx); 2520 mXScrollAnimator = new ValueAnimator(); 2521 mXScrollAnimator.addUpdateListener(mXScrollAnimatorUpdateListener); 2522 mXScrollAnimator.addListener(mXScrollAnimatorListener); 2523 mXScrollAnimator.setInterpolator(interpolator); 2524 } 2525 2526 public void fling( 2527 int startX, int startY, 2528 int velocityX, int velocityY, 2529 int minX, int maxX, 2530 int minY, int maxY) { 2531 mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); 2532 runChecker(); 2533 } 2534 2535 public void startScroll(int startX, int startY, int dx, int dy) { 2536 mScroller.startScroll(startX, startY, dx, dy); 2537 runChecker(); 2538 } 2539 2540 /** Only starts and updates scroll in x-axis. */ 2541 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 2542 mXScrollAnimator.cancel(); 2543 mXScrollAnimator.setDuration(duration); 2544 mXScrollAnimator.setIntValues(startX, startX + dx); 2545 mXScrollAnimator.start(); 2546 } 2547 2548 public boolean isFinished() { 2549 return (mScroller.isFinished() && !mXScrollAnimator.isRunning()); 2550 } 2551 2552 public void forceFinished(boolean finished) { 2553 mScroller.forceFinished(finished); 2554 if (finished) { 2555 mXScrollAnimator.cancel(); 2556 } 2557 } 2558 2559 private void runChecker() { 2560 if (mHandler == null || mListener == null) { 2561 return; 2562 } 2563 mHandler.removeCallbacks(mScrollChecker); 2564 mHandler.post(mScrollChecker); 2565 } 2566 } 2567 2568 private class MyGestureReceiver implements FilmstripGestureRecognizer.Listener { 2569 2570 private static final int SCROLL_DIR_NONE = 0; 2571 private static final int SCROLL_DIR_VERTICAL = 1; 2572 private static final int SCROLL_DIR_HORIZONTAL = 2; 2573 // Indicating the current trend of scaling is up (>1) or down (<1). 2574 private float mScaleTrend; 2575 private float mMaxScale; 2576 private int mScrollingDirection = SCROLL_DIR_NONE; 2577 private long mLastDownTime; 2578 private float mLastDownY; 2579 2580 @Override 2581 public boolean onSingleTapUp(float x, float y) { 2582 ViewItem centerItem = mViewItem[mCurrentItem]; 2583 if (inFilmstrip()) { 2584 if (centerItem != null && centerItem.areaContains(x, y)) { 2585 mController.goToFullScreen(); 2586 return true; 2587 } 2588 } else if (inFullScreen()) { 2589 if (mFullScreenUIHidden) { 2590 onLeaveFullScreenUiHidden(); 2591 onEnterFullScreen(); 2592 } else { 2593 onLeaveFullScreen(); 2594 onEnterFullScreenUiHidden(); 2595 } 2596 return true; 2597 } 2598 return false; 2599 } 2600 2601 @Override 2602 public boolean onDoubleTap(float x, float y) { 2603 ViewItem current = mViewItem[mCurrentItem]; 2604 if (current == null) { 2605 return false; 2606 } 2607 if (inFilmstrip()) { 2608 mController.goToFullScreen(); 2609 return true; 2610 } else if (mScale < FULL_SCREEN_SCALE || inCameraFullscreen()) { 2611 return false; 2612 } 2613 if (!mController.stopScrolling(false)) { 2614 return false; 2615 } 2616 if (inFullScreen()) { 2617 mController.zoomAt(current, x, y); 2618 checkItemAtMaxSize(); 2619 return true; 2620 } else if (mScale > FULL_SCREEN_SCALE) { 2621 // In zoom view. 2622 mController.zoomAt(current, x, y); 2623 } 2624 return false; 2625 } 2626 2627 @Override 2628 public boolean onDown(float x, float y) { 2629 mLastDownTime = SystemClock.uptimeMillis(); 2630 mLastDownY = y; 2631 mController.cancelFlingAnimation(); 2632 if (!mController.stopScrolling(false)) { 2633 return false; 2634 } 2635 2636 return true; 2637 } 2638 2639 @Override 2640 public boolean onUp(float x, float y) { 2641 ViewItem currItem = mViewItem[mCurrentItem]; 2642 if (currItem == null) { 2643 return false; 2644 } 2645 if (mController.isZoomAnimationRunning() || mController.isFlingAnimationRunning()) { 2646 return false; 2647 } 2648 if (inZoomView()) { 2649 mController.loadZoomedImage(); 2650 return true; 2651 } 2652 float promoteHeight = getHeight() * PROMOTE_HEIGHT_RATIO; 2653 float velocityPromoteHeight = getHeight() * VELOCITY_PROMOTE_HEIGHT_RATIO; 2654 mIsUserScrolling = false; 2655 mScrollingDirection = SCROLL_DIR_NONE; 2656 // Finds items promoted/demoted. 2657 float speedY = Math.abs(y - mLastDownY) 2658 / (SystemClock.uptimeMillis() - mLastDownTime); 2659 for (int i = 0; i < BUFFER_SIZE; i++) { 2660 if (mViewItem[i] == null) { 2661 continue; 2662 } 2663 float transY = mViewItem[i].getTranslationY(); 2664 if (transY == 0) { 2665 continue; 2666 } 2667 int id = mViewItem[i].getId(); 2668 2669 if (mDataAdapter.getImageData(id) 2670 .isUIActionSupported(ImageData.ACTION_DEMOTE) 2671 && ((transY > promoteHeight) 2672 || (transY > velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) { 2673 demoteData(i, id); 2674 } else if (mDataAdapter.getImageData(id) 2675 .isUIActionSupported(ImageData.ACTION_PROMOTE) 2676 && (transY < -promoteHeight 2677 || (transY < -velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) { 2678 promoteData(i, id); 2679 } else { 2680 // put the view back. 2681 slideViewBack(mViewItem[i]); 2682 } 2683 } 2684 2685 // The data might be changed. Re-check. 2686 currItem = mViewItem[mCurrentItem]; 2687 if (currItem == null) { 2688 return true; 2689 } 2690 2691 int currId = currItem.getId(); 2692 if (mCenterX > currItem.getCenterX() + CAMERA_PREVIEW_SWIPE_THRESHOLD && currId == 0 && 2693 isViewTypeSticky(currItem) && mDataIdOnUserScrolling == 0) { 2694 mController.goToFilmstrip(); 2695 // Special case to go from camera preview to the next photo. 2696 if (mViewItem[mCurrentItem + 1] != null) { 2697 mController.scrollToPosition( 2698 mViewItem[mCurrentItem + 1].getCenterX(), 2699 GEOMETRY_ADJUST_TIME_MS, false); 2700 } else { 2701 // No next photo. 2702 snapInCenter(); 2703 } 2704 } 2705 if (isCurrentItemCentered() && currId == 0 && isViewTypeSticky(currItem)) { 2706 mController.goToFullScreen(); 2707 } else { 2708 if (mDataIdOnUserScrolling == 0 && currId != 0) { 2709 // Special case to go to filmstrip when the user scroll away 2710 // from the camera preview and the current one is not the 2711 // preview anymore. 2712 mController.goToFilmstrip(); 2713 mDataIdOnUserScrolling = currId; 2714 } 2715 snapInCenter(); 2716 } 2717 return false; 2718 } 2719 2720 @Override 2721 public void onLongPress(float x, float y) { 2722 final int dataId = getCurrentId(); 2723 if (dataId == -1) { 2724 return; 2725 } 2726 mListener.onFocusedDataLongPressed(dataId); 2727 } 2728 2729 @Override 2730 public boolean onScroll(float x, float y, float dx, float dy) { 2731 final ViewItem currItem = mViewItem[mCurrentItem]; 2732 if (currItem == null) { 2733 return false; 2734 } 2735 if (inFullScreen() && !mDataAdapter.canSwipeInFullScreen(currItem.getId())) { 2736 return false; 2737 } 2738 hideZoomView(); 2739 // When image is zoomed in to be bigger than the screen 2740 if (inZoomView()) { 2741 ViewItem curr = mViewItem[mCurrentItem]; 2742 float transX = curr.getTranslationX() * mScale - dx; 2743 float transY = curr.getTranslationY() * mScale - dy; 2744 curr.updateTransform(transX, transY, mScale, mScale, mDrawArea.width(), 2745 mDrawArea.height()); 2746 return true; 2747 } 2748 int deltaX = (int) (dx / mScale); 2749 // Forces the current scrolling to stop. 2750 mController.stopScrolling(true); 2751 if (!mIsUserScrolling) { 2752 mIsUserScrolling = true; 2753 mDataIdOnUserScrolling = mViewItem[mCurrentItem].getId(); 2754 } 2755 if (inFilmstrip()) { 2756 // Disambiguate horizontal/vertical first. 2757 if (mScrollingDirection == SCROLL_DIR_NONE) { 2758 mScrollingDirection = (Math.abs(dx) > Math.abs(dy)) ? SCROLL_DIR_HORIZONTAL : 2759 SCROLL_DIR_VERTICAL; 2760 } 2761 if (mScrollingDirection == SCROLL_DIR_HORIZONTAL) { 2762 if (mCenterX == currItem.getCenterX() && currItem.getId() == 0 && dx < 0) { 2763 // Already at the beginning, don't process the swipe. 2764 mIsUserScrolling = false; 2765 mScrollingDirection = SCROLL_DIR_NONE; 2766 return false; 2767 } 2768 mController.scroll(deltaX); 2769 } else { 2770 // Vertical part. Promote or demote. 2771 int hit = 0; 2772 Rect hitRect = new Rect(); 2773 for (; hit < BUFFER_SIZE; hit++) { 2774 if (mViewItem[hit] == null) { 2775 continue; 2776 } getHitRect(hitRect)2777 mViewItem[hit].getHitRect(hitRect); 2778 if (hitRect.contains((int) x, (int) y)) { 2779 break; 2780 } 2781 } 2782 if (hit == BUFFER_SIZE) { 2783 // Hit none. 2784 return true; 2785 } 2786 2787 ImageData data = mDataAdapter.getImageData(mViewItem[hit].getId()); 2788 float transY = mViewItem[hit].getTranslationY() - dy / mScale; 2789 if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) && 2790 transY > 0f) { 2791 transY = 0f; 2792 } 2793 if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) && 2794 transY < 0f) { 2795 transY = 0f; 2796 } setTranslationY(transY)2797 mViewItem[hit].setTranslationY(transY); 2798 } 2799 } else if (inFullScreen()) { 2800 if (mViewItem[mCurrentItem] == null || (deltaX < 0 && mCenterX <= 2801 currItem.getCenterX() && currItem.getId() == 0)) { 2802 return false; 2803 } 2804 // Multiplied by 1.2 to make it more easy to swipe. 2805 mController.scroll((int) (deltaX * 1.2)); 2806 } invalidate()2807 invalidate(); 2808 2809 return true; 2810 } 2811 2812 @Override onFling(float velocityX, float velocityY)2813 public boolean onFling(float velocityX, float velocityY) { 2814 final ViewItem currItem = mViewItem[mCurrentItem]; 2815 if (currItem == null) { 2816 return false; 2817 } 2818 if (!mDataAdapter.canSwipeInFullScreen(currItem.getId())) { 2819 return false; 2820 } 2821 if (inZoomView()) { 2822 // Fling within the zoomed image 2823 mController.flingInsideZoomView(velocityX, velocityY); 2824 return true; 2825 } 2826 if (Math.abs(velocityX) < Math.abs(velocityY)) { 2827 // ignore vertical fling. 2828 return true; 2829 } 2830 2831 // In full-screen, fling of a velocity above a threshold should go 2832 // to the next/prev photos 2833 if (mScale == FULL_SCREEN_SCALE) { 2834 int currItemCenterX = currItem.getCenterX(); 2835 2836 if (velocityX > 0) { // left 2837 if (mCenterX > currItemCenterX) { 2838 // The visually previous item is actually the current 2839 // item. 2840 mController.scrollToPosition( 2841 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true); 2842 return true; 2843 } 2844 ViewItem prevItem = mViewItem[mCurrentItem - 1]; 2845 if (prevItem == null) { 2846 return false; 2847 } 2848 mController.scrollToPosition( 2849 prevItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true); 2850 } else { // right 2851 if (mController.stopScrolling(false)) { 2852 if (mCenterX < currItemCenterX) { 2853 // The visually next item is actually the current 2854 // item. 2855 mController.scrollToPosition( 2856 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true); 2857 return true; 2858 } 2859 final ViewItem nextItem = mViewItem[mCurrentItem + 1]; 2860 if (nextItem == null) { 2861 return false; 2862 } 2863 mController.scrollToPosition( 2864 nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true); 2865 if (isViewTypeSticky(currItem)) { 2866 mController.goToFilmstrip(); 2867 } 2868 } 2869 } 2870 } 2871 2872 if (mScale == FILM_STRIP_SCALE) { 2873 mController.fling(velocityX); 2874 } 2875 return true; 2876 } 2877 2878 @Override onScaleBegin(float focusX, float focusY)2879 public boolean onScaleBegin(float focusX, float focusY) { 2880 if (inCameraFullscreen()) { 2881 return false; 2882 } 2883 2884 hideZoomView(); 2885 mScaleTrend = 1f; 2886 // If the image is smaller than screen size, we should allow to zoom 2887 // in to full screen size 2888 mMaxScale = Math.max(mController.getCurrentDataMaxScale(true), FULL_SCREEN_SCALE); 2889 return true; 2890 } 2891 2892 @Override onScale(float focusX, float focusY, float scale)2893 public boolean onScale(float focusX, float focusY, float scale) { 2894 if (inCameraFullscreen()) { 2895 return false; 2896 } 2897 2898 mScaleTrend = mScaleTrend * 0.3f + scale * 0.7f; 2899 float newScale = mScale * scale; 2900 if (mScale < FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) { 2901 if (newScale <= FILM_STRIP_SCALE) { 2902 newScale = FILM_STRIP_SCALE; 2903 } 2904 // Scaled view is smaller than or equal to screen size both 2905 // before and after scaling 2906 if (mScale != newScale) { 2907 if (mScale == FILM_STRIP_SCALE) { 2908 onLeaveFilmstrip(); 2909 } 2910 if (newScale == FILM_STRIP_SCALE) { 2911 onEnterFilmstrip(); 2912 } 2913 } 2914 mScale = newScale; 2915 invalidate(); 2916 } else if (mScale < FULL_SCREEN_SCALE && newScale >= FULL_SCREEN_SCALE) { 2917 // Going from smaller than screen size to bigger than or equal 2918 // to screen size 2919 if (mScale == FILM_STRIP_SCALE) { 2920 onLeaveFilmstrip(); 2921 } 2922 mScale = FULL_SCREEN_SCALE; 2923 onEnterFullScreen(); 2924 mController.setSurroundingViewsVisible(false); 2925 invalidate(); 2926 } else if (mScale >= FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) { 2927 // Going from bigger than or equal to screen size to smaller 2928 // than screen size 2929 if (inFullScreen()) { 2930 if (mFullScreenUIHidden) { 2931 onLeaveFullScreenUiHidden(); 2932 } else { 2933 onLeaveFullScreen(); 2934 } 2935 } else { 2936 onLeaveZoomView(); 2937 } 2938 mScale = newScale; 2939 onEnterFilmstrip(); 2940 invalidate(); 2941 } else { 2942 // Scaled view bigger than or equal to screen size both before 2943 // and after scaling 2944 if (!inZoomView()) { 2945 mController.setSurroundingViewsVisible(false); 2946 } 2947 ViewItem curr = mViewItem[mCurrentItem]; 2948 // Make sure the image is not overly scaled 2949 newScale = Math.min(newScale, mMaxScale); 2950 if (newScale == mScale) { 2951 return true; 2952 } 2953 float postScale = newScale / mScale; 2954 curr.postScale(focusX, focusY, postScale, mDrawArea.width(), mDrawArea.height()); 2955 mScale = newScale; 2956 if (mScale == FULL_SCREEN_SCALE) { 2957 onEnterFullScreen(); 2958 } else { 2959 onEnterZoomView(); 2960 } 2961 checkItemAtMaxSize(); 2962 } 2963 return true; 2964 } 2965 2966 @Override onScaleEnd()2967 public void onScaleEnd() { 2968 zoomAtIndexChanged(); 2969 if (mScale > FULL_SCREEN_SCALE + TOLERANCE) { 2970 return; 2971 } 2972 mController.setSurroundingViewsVisible(true); 2973 if (mScale <= FILM_STRIP_SCALE + TOLERANCE) { 2974 mController.goToFilmstrip(); 2975 } else if (mScaleTrend > 1f || mScale > FULL_SCREEN_SCALE - TOLERANCE) { 2976 if (inZoomView()) { 2977 mScale = FULL_SCREEN_SCALE; 2978 resetZoomView(); 2979 } 2980 mController.goToFullScreen(); 2981 } else { 2982 mController.goToFilmstrip(); 2983 } 2984 mScaleTrend = 1f; 2985 } 2986 } 2987 } 2988