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