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