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