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