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