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