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