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