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