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