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