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