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