2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.camera.widget;
19 import android.animation.Animator;
20 import android.animation.AnimatorSet;
21 import android.animation.TimeInterpolator;
22 import android.animation.ValueAnimator;
23 import android.annotation.TargetApi;
24 import android.app.Activity;
25 import android.content.Context;
26 import android.graphics.Canvas;
27 import android.graphics.Point;
28 import android.graphics.Rect;
29 import android.graphics.RectF;
30 import android.net.Uri;
31 import android.os.Build;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.os.SystemClock;
35 import android.util.AttributeSet;
36 import android.util.DisplayMetrics;
37 import android.util.SparseArray;
38 import android.view.MotionEvent;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.accessibility.AccessibilityNodeInfo;
42 import android.view.animation.DecelerateInterpolator;
43 import android.widget.Scroller;
45 import com.android.camera.CameraActivity;
46 import com.android.camera.data.LocalData.ActionCallback;
47 import com.android.camera.debug.Log;
48 import com.android.camera.filmstrip.DataAdapter;
49 import com.android.camera.filmstrip.FilmstripController;
50 import com.android.camera.filmstrip.ImageData;
51 import com.android.camera.ui.FilmstripGestureRecognizer;
52 import com.android.camera.ui.ZoomView;
53 import com.android.camera.util.ApiHelper;
54 import com.android.camera.util.CameraUtil;
55 import com.android.camera2.R;
57 import java.lang.ref.WeakReference;
58 import java.util.ArrayDeque;
59 import java.util.Arrays;
60 import java.util.Queue;
62 public class FilmstripView extends ViewGroup {
64 * An action callback to be used for actions on the local media data items.
66 public static class ActionCallbackImpl implements ActionCallback {
67 private final WeakReference<Activity> mActivity;
70 * The given activity is used to start intents. It is wrapped in a weak
71 * reference to prevent leaks.
73 public ActionCallbackImpl(Activity activity) {
74 mActivity = new WeakReference<Activity>(activity);
78 * Fires an intent to play the video with the given URI and title.
81 public void playVideo(Uri uri, String title) {
82 Activity activity = mActivity.get();
83 if (activity != null) {
84 CameraUtil.playVideo(activity, uri, title);
90 private static final Log.Tag TAG = new Log.Tag("FilmstripView");
92 private static final int BUFFER_SIZE = 5;
93 private static final int GEOMETRY_ADJUST_TIME_MS = 400;
94 private static final int SNAP_IN_CENTER_TIME_MS = 600;
95 private static final float FLING_COASTING_DURATION_S = 0.05f;
96 private static final int ZOOM_ANIMATION_DURATION_MS = 200;
97 private static final int CAMERA_PREVIEW_SWIPE_THRESHOLD = 300;
98 private static final float FILM_STRIP_SCALE = 0.7f;
99 private static final float FULL_SCREEN_SCALE = 1f;
101 // The min velocity at which the user must have moved their finger in
102 // pixels per millisecond to count a vertical gesture as a promote/demote
103 // at short vertical distances.
104 private static final float PROMOTE_VELOCITY = 3.5f;
105 // The min distance relative to this view's height the user must have
106 // moved their finger to count a vertical gesture as a promote/demote if
107 // they moved their finger at least at PROMOTE_VELOCITY.
108 private static final float VELOCITY_PROMOTE_HEIGHT_RATIO = 1/10f;
109 // The min distance relative to this view's height the user must have
110 // moved their finger to count a vertical gesture as a promote/demote if
111 // they moved their finger at less than PROMOTE_VELOCITY.
112 private static final float PROMOTE_HEIGHT_RATIO = 1/2f;
114 private static final float TOLERANCE = 0.1f;
115 // Only check for intercepting touch events within first 500ms
116 private static final int SWIPE_TIME_OUT = 500;
117 private static final int DECELERATION_FACTOR = 4;
119 private CameraActivity mActivity;
120 private ActionCallback mActionCallback;
121 private FilmstripGestureRecognizer mGestureRecognizer;
122 private FilmstripGestureRecognizer.Listener mGestureListener;
123 private DataAdapter mDataAdapter;
124 private int mViewGapInPixel;
125 private final Rect mDrawArea = new Rect();
127 private final int mCurrentItem = (BUFFER_SIZE - 1) / 2;
128 private float mScale;
129 private MyController mController;
130 private int mCenterX = -1;
131 private final ViewItem[] mViewItem = new ViewItem[BUFFER_SIZE];
133 private FilmstripController.FilmstripListener mListener;
134 private ZoomView mZoomView = null;
136 private MotionEvent mDown;
137 private boolean mCheckToIntercept = true;
139 private TimeInterpolator mViewAnimInterpolator;
141 // This is true if and only if the user is scrolling,
142 private boolean mIsUserScrolling;
143 private int mDataIdOnUserScrolling;
144 private float mOverScaleFactor = 1f;
146 private boolean mFullScreenUIHidden = false;
147 private final SparseArray<Queue<View>> recycledViews = new SparseArray<Queue<View>>();
150 * A helper class to tract and calculate the view coordination.
152 private class ViewItem {
154 /** The position of the left of the view in the whole filmstrip. */
155 private int mLeftPosition;
156 private final View mView;
157 private final ImageData mData;
158 private final RectF mViewArea;
159 private boolean mMaximumBitmapRequested;
161 private ValueAnimator mTranslationXAnimator;
162 private ValueAnimator mTranslationYAnimator;
163 private ValueAnimator mAlphaAnimator;
168 * @param id The id of the data from
169 * {@link com.android.camera.filmstrip.DataAdapter}.
170 * @param v The {@code View} representing the data.
172 public ViewItem(int id, View v, ImageData data) {
178 mMaximumBitmapRequested = false;
180 mViewArea = new RectF();
183 public boolean isMaximumBitmapRequested() {
184 return mMaximumBitmapRequested;
187 public void setMaximumBitmapRequested() {
188 mMaximumBitmapRequested = true;
192 * Returns the data id from
193 * {@link com.android.camera.filmstrip.DataAdapter}.
200 * Sets the data id from
201 * {@link com.android.camera.filmstrip.DataAdapter}.
203 public void setId(int id) {
207 /** Sets the left position of the view in the whole filmstrip. */
208 public void setLeftPosition(int pos) {
212 /** Returns the left position of the view in the whole filmstrip. */
213 public int getLeftPosition() {
214 return mLeftPosition;
217 /** Returns the translation of Y regarding the view scale. */
218 public float getTranslationY() {
219 return mView.getTranslationY() / mScale;
222 /** Returns the translation of X regarding the view scale. */
223 public float getTranslationX() {
224 return mView.getTranslationX() / mScale;
227 /** Sets the translation of Y regarding the view scale. */
228 public void setTranslationY(float transY) {
229 mView.setTranslationY(transY * mScale);
232 /** Sets the translation of X regarding the view scale. */
233 public void setTranslationX(float transX) {
234 mView.setTranslationX(transX * mScale);
237 /** Forwarding of {@link android.view.View#setAlpha(float)}. */
238 public void setAlpha(float alpha) {
239 mView.setAlpha(alpha);
242 /** Forwarding of {@link android.view.View#getAlpha()}. */
243 public float getAlpha() {
244 return mView.getAlpha();
247 /** Forwarding of {@link android.view.View#getMeasuredWidth()}. */
248 public int getMeasuredWidth() {
249 return mView.getMeasuredWidth();
253 * Animates the X translation of the view. Note: the animated value is
254 * not set directly by {@link android.view.View#setTranslationX(float)}
255 * because the value might be changed during in {@code onLayout()}.
256 * The animated value of X translation is specially handled in {@code
259 * @param targetX The final value.
260 * @param duration_ms The duration of the animation.
261 * @param interpolator Time interpolator.
263 public void animateTranslationX(
264 float targetX, long duration_ms, TimeInterpolator interpolator) {
265 if (mTranslationXAnimator == null) {
266 mTranslationXAnimator = new ValueAnimator();
267 mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
269 public void onAnimationUpdate(ValueAnimator valueAnimator) {
270 // We invalidate the filmstrip view instead of setting the
271 // translation X because the translation X of the view is
272 // touched in onLayout(). See the documentation of
273 // animateTranslationX().
278 runAnimation(mTranslationXAnimator, getTranslationX(), targetX, duration_ms,
283 * Animates the Y translation of the view.
285 * @param targetY The final value.
286 * @param duration_ms The duration of the animation.
287 * @param interpolator Time interpolator.
289 public void animateTranslationY(
290 float targetY, long duration_ms, TimeInterpolator interpolator) {
291 if (mTranslationYAnimator == null) {
292 mTranslationYAnimator = new ValueAnimator();
293 mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
295 public void onAnimationUpdate(ValueAnimator valueAnimator) {
296 setTranslationY((Float) valueAnimator.getAnimatedValue());
300 runAnimation(mTranslationYAnimator, getTranslationY(), targetY, duration_ms,
305 * Animates the alpha value of the view.
307 * @param targetAlpha The final value.
308 * @param duration_ms The duration of the animation.
309 * @param interpolator Time interpolator.
311 public void animateAlpha(float targetAlpha, long duration_ms,
312 TimeInterpolator interpolator) {
313 if (mAlphaAnimator == null) {
314 mAlphaAnimator = new ValueAnimator();
315 mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
317 public void onAnimationUpdate(ValueAnimator valueAnimator) {
318 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue());
322 runAnimation(mAlphaAnimator, getAlpha(), targetAlpha, duration_ms, interpolator);
325 private void runAnimation(final ValueAnimator animator, final float startValue,
326 final float targetValue, final long duration_ms,
327 final TimeInterpolator interpolator) {
328 if (startValue == targetValue) {
331 animator.setInterpolator(interpolator);
332 animator.setDuration(duration_ms);
333 animator.setFloatValues(startValue, targetValue);
337 /** Adjusts the translation of X regarding the view scale. */
338 public void translateXScaledBy(float transX) {
339 setTranslationX(getTranslationX() + transX * mScale);
343 * Forwarding of {@link android.view.View#getHitRect(android.graphics.Rect)}.
345 public void getHitRect(Rect rect) {
346 mView.getHitRect(rect);
349 public int getCenterX() {
350 return mLeftPosition + mView.getMeasuredWidth() / 2;
353 /** Forwarding of {@link android.view.View#getVisibility()}. */
354 public int getVisibility() {
355 return mView.getVisibility();
358 /** Forwarding of {@link android.view.View#setVisibility(int)}. */
359 public void setVisibility(int visibility) {
360 mView.setVisibility(visibility);
364 * Notifies the {@link com.android.camera.filmstrip.DataAdapter} to
367 public void resizeView(Context context, int w, int h) {
368 mDataAdapter.resizeView(context, mDataId, mView, w, h);
372 * Adds the view of the data to the view hierarchy if necessary.
374 public void addViewToHierarchy() {
375 if (indexOfChild(mView) < 0) {
380 setVisibility(View.VISIBLE);
387 * Removes from the hierarchy. Keeps the view in the view hierarchy if
388 * view type is {@code VIEW_TYPE_STICKY} and set to invisible instead.
390 * @param force {@code true} to remove the view from the hierarchy
391 * regardless of the view type.
393 public void removeViewFromHierarchy(boolean force) {
394 if (force || mData.getViewType() != ImageData.VIEW_TYPE_STICKY) {
396 mData.recycle(mView);
397 recycleView(mView, mDataId);
399 setVisibility(View.INVISIBLE);
404 * Brings the view to front by
405 * {@link #bringChildToFront(android.view.View)}
407 public void bringViewToFront() {
408 bringChildToFront(mView);
412 * The visual x position of this view, in pixels.
414 public float getX() {
419 * The visual y position of this view, in pixels.
421 public float getY() {
426 * Forwarding of {@link android.view.View#measure(int, int)}.
428 public void measure(int widthSpec, int heightSpec) {
429 mView.measure(widthSpec, heightSpec);
432 private void layoutAt(int left, int top) {
433 mView.layout(left, top, left + mView.getMeasuredWidth(),
434 top + mView.getMeasuredHeight());
438 * The bounding rect of the view.
440 public RectF getViewRect() {
441 RectF r = new RectF();
442 r.left = mView.getX();
443 r.top = mView.getY();
444 r.right = r.left + mView.getWidth() * mView.getScaleX();
445 r.bottom = r.top + mView.getHeight() * mView.getScaleY();
449 private View getView() {
454 * Layouts the view in the area assuming the center of the area is at a
455 * specific point of the whole filmstrip.
457 * @param drawArea The area when filmstrip will show in.
458 * @param refCenter The absolute X coordination in the whole filmstrip
459 * of the center of {@code drawArea}.
460 * @param scale The scale of the view on the filmstrip.
462 public void layoutWithTranslationX(Rect drawArea, int refCenter, float scale) {
463 final float translationX =
464 ((mTranslationXAnimator != null && mTranslationXAnimator.isRunning()) ?
465 (Float) mTranslationXAnimator.getAnimatedValue() : 0);
467 (int) (drawArea.centerX() + (mLeftPosition - refCenter + translationX) * scale);
468 int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale);
470 mView.setScaleX(scale);
471 mView.setScaleY(scale);
473 // update mViewArea for touch detection.
474 int l = mView.getLeft();
475 int t = mView.getTop();
477 l + mView.getMeasuredWidth() * scale,
478 t + mView.getMeasuredHeight() * scale);
481 /** Returns true if the point is in the view. */
482 public boolean areaContains(float x, float y) {
483 return mViewArea.contains(x, y);
487 * Return the width of the view.
489 public int getWidth() {
490 return mView.getWidth();
494 * Returns the position of the left edge of the view area content is drawn in.
496 public int getDrawAreaLeft() {
497 return Math.round(mViewArea.left);
500 public void copyAttributes(ViewItem item) {
501 setLeftPosition(item.getLeftPosition());
503 setTranslationX(item.getTranslationX());
504 if (item.mTranslationXAnimator != null) {
505 mTranslationXAnimator = item.mTranslationXAnimator;
506 mTranslationXAnimator.removeAllUpdateListeners();
507 mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
509 public void onAnimationUpdate(ValueAnimator valueAnimator) {
510 // We invalidate the filmstrip view instead of setting the
511 // translation X because the translation X of the view is
512 // touched in onLayout(). See the documentation of
513 // animateTranslationX().
519 setTranslationY(item.getTranslationY());
520 if (item.mTranslationYAnimator != null) {
521 mTranslationYAnimator = item.mTranslationYAnimator;
522 mTranslationYAnimator.removeAllUpdateListeners();
523 mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
525 public void onAnimationUpdate(ValueAnimator valueAnimator) {
526 setTranslationY((Float) valueAnimator.getAnimatedValue());
531 setAlpha(item.getAlpha());
532 if (item.mAlphaAnimator != null) {
533 mAlphaAnimator = item.mAlphaAnimator;
534 mAlphaAnimator.removeAllUpdateListeners();
535 mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
537 public void onAnimationUpdate(ValueAnimator valueAnimator) {
538 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue());
545 * Apply a scale factor (i.e. {@code postScale}) on top of current scale at
546 * pivot point ({@code focusX}, {@code focusY}). Visually it should be the
547 * same as post concatenating current view's matrix with specified scale.
549 void postScale(float focusX, float focusY, float postScale, int viewportWidth,
550 int viewportHeight) {
551 float transX = mView.getTranslationX();
552 float transY = mView.getTranslationY();
553 // Pivot point is top left of the view, so we need to translate
554 // to scale around focus point
555 transX -= (focusX - getX()) * (postScale - 1f);
556 transY -= (focusY - getY()) * (postScale - 1f);
557 float scaleX = mView.getScaleX() * postScale;
558 float scaleY = mView.getScaleY() * postScale;
559 updateTransform(transX, transY, scaleX, scaleY, viewportWidth,
563 void updateTransform(float transX, float transY, float scaleX, float scaleY,
564 int viewportWidth, int viewportHeight) {
565 float left = transX + mView.getLeft();
566 float top = transY + mView.getTop();
567 RectF r = ZoomView.adjustToFitInBounds(new RectF(left, top,
568 left + mView.getWidth() * scaleX,
569 top + mView.getHeight() * scaleY),
570 viewportWidth, viewportHeight);
571 mView.setScaleX(scaleX);
572 mView.setScaleY(scaleY);
573 transX = r.left - mView.getLeft();
574 transY = r.top - mView.getTop();
575 mView.setTranslationX(transX);
576 mView.setTranslationY(transY);
579 void resetTransform() {
580 mView.setScaleX(FULL_SCREEN_SCALE);
581 mView.setScaleY(FULL_SCREEN_SCALE);
582 mView.setTranslationX(0f);
583 mView.setTranslationY(0f);
587 public String toString() {
588 return "DataID = " + mDataId + "\n\t left = " + mLeftPosition
589 + "\n\t viewArea = " + mViewArea
590 + "\n\t centerX = " + getCenterX()
591 + "\n\t view MeasuredSize = "
592 + mView.getMeasuredWidth() + ',' + mView.getMeasuredHeight()
593 + "\n\t view Size = " + mView.getWidth() + ',' + mView.getHeight()
594 + "\n\t view scale = " + mView.getScaleX();
599 public FilmstripView(Context context) {
601 init((CameraActivity) context);
605 public FilmstripView(Context context, AttributeSet attrs) {
606 super(context, attrs);
607 init((CameraActivity) context);
611 public FilmstripView(Context context, AttributeSet attrs, int defStyle) {
612 super(context, attrs, defStyle);
613 init((CameraActivity) context);
616 private void init(CameraActivity cameraActivity) {
617 setWillNotDraw(false);
618 mActivity = cameraActivity;
619 mActionCallback = new ActionCallbackImpl(mActivity);
621 mDataIdOnUserScrolling = 0;
622 mController = new MyController(cameraActivity);
623 mViewAnimInterpolator = new DecelerateInterpolator();
624 mZoomView = new ZoomView(cameraActivity);
625 mZoomView.setVisibility(GONE);
628 mGestureListener = new MyGestureReceiver();
630 new FilmstripGestureRecognizer(cameraActivity, mGestureListener);
631 mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop);
632 DisplayMetrics metrics = new DisplayMetrics();
633 mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
634 // Allow over scaling because on high density screens, pixels are too
635 // tiny to clearly see the details at 1:1 zoom. We should not scale
636 // beyond what 1:1 would look like on a medium density screen, as
637 // scaling beyond that would only yield blur.
638 mOverScaleFactor = (float) metrics.densityDpi / (float) DisplayMetrics.DENSITY_HIGH;
639 if (mOverScaleFactor < 1f) {
640 mOverScaleFactor = 1f;
643 setAccessibilityDelegate(new AccessibilityDelegate() {
645 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
646 super.onInitializeAccessibilityNodeInfo(host, info);
648 info.setClassName(FilmstripView.class.getName());
649 info.setScrollable(true);
650 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
651 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
655 public boolean performAccessibilityAction(View host, int action, Bundle args) {
656 if (!mController.isScrolling()) {
658 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
659 mController.goToNextItem();
662 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
663 boolean wentToPrevious = mController.goToPreviousItem();
664 if (!wentToPrevious) {
665 // at beginning of filmstrip, hide and go back to preview
666 mActivity.getCameraAppUI().hideFilmstrip();
670 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
671 // Prevent the view group itself from being selected.
672 // Instead, select the item in the center
673 final ViewItem currentItem = mViewItem[mCurrentItem];
674 currentItem.getView().performAccessibilityAction(action, args);
679 return super.performAccessibilityAction(host, action, args);
684 private void recycleView(View view, int dataId) {
685 final int viewType = (Integer) view.getTag(R.id.mediadata_tag_viewtype);
687 Queue<View> recycledViewsForType = recycledViews.get(viewType);
688 if (recycledViewsForType == null) {
689 recycledViewsForType = new ArrayDeque<View>();
690 recycledViews.put(viewType, recycledViewsForType);
692 recycledViewsForType.offer(view);
696 private View getRecycledView(int dataId) {
697 final int viewType = mDataAdapter.getItemViewType(dataId);
698 Queue<View> recycledViewsForType = recycledViews.get(viewType);
700 if (recycledViewsForType != null) {
701 result = recycledViewsForType.poll();
707 * Returns the controller.
709 * @return The {@code Controller}.
711 public FilmstripController getController() {
716 * Returns the draw area width of the current item.
718 public int getCurrentItemLeft() {
719 return mViewItem[mCurrentItem].getDrawAreaLeft();
722 private void setListener(FilmstripController.FilmstripListener l) {
726 private void setViewGap(int viewGap) {
727 mViewGapInPixel = viewGap;
731 * Called after current item or zoom level has changed.
733 public void zoomAtIndexChanged() {
734 if (mViewItem[mCurrentItem] == null) {
737 int id = mViewItem[mCurrentItem].getId();
738 mListener.onZoomAtIndexChanged(id, mScale);
742 * Checks if the data is at the center.
744 * @param id The id of the data to check.
745 * @return {@code True} if the data is currently at the center.
747 private boolean isDataAtCenter(int id) {
748 if (mViewItem[mCurrentItem] == null) {
751 if (mViewItem[mCurrentItem].getId() == id
752 && isCurrentItemCentered()) {
758 private void measureViewItem(ViewItem item, int boundWidth, int boundHeight) {
759 int id = item.getId();
760 ImageData imageData = mDataAdapter.getImageData(id);
761 if (imageData == null) {
762 Log.e(TAG, "trying to measure a null item");
766 Point dim = CameraUtil.resizeToFill(imageData.getWidth(), imageData.getHeight(),
767 imageData.getRotation(), boundWidth, boundHeight);
769 item.measure(MeasureSpec.makeMeasureSpec(dim.x, MeasureSpec.EXACTLY),
770 MeasureSpec.makeMeasureSpec(dim.y, MeasureSpec.EXACTLY));
774 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
775 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
777 int boundWidth = MeasureSpec.getSize(widthMeasureSpec);
778 int boundHeight = MeasureSpec.getSize(heightMeasureSpec);
779 if (boundWidth == 0 || boundHeight == 0) {
780 // Either width or height is unknown, can't measure children yet.
784 for (ViewItem item : mViewItem) {
786 measureViewItem(item, boundWidth, boundHeight);
791 mZoomView.measure(MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.EXACTLY),
792 MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.EXACTLY));
795 private int findTheNearestView(int pointX) {
798 // Find the first non-null ViewItem.
799 while (nearest < BUFFER_SIZE
800 && (mViewItem[nearest] == null || mViewItem[nearest].getLeftPosition() == -1)) {
803 // No existing available ViewItem
804 if (nearest == BUFFER_SIZE) {
808 int min = Math.abs(pointX - mViewItem[nearest].getCenterX());
810 for (int itemID = nearest + 1; itemID < BUFFER_SIZE && mViewItem[itemID] != null; itemID++) {
812 if (mViewItem[itemID].getLeftPosition() == -1) {
816 int c = mViewItem[itemID].getCenterX();
817 int dist = Math.abs(pointX - c);
826 private ViewItem buildItemFromData(int dataID) {
827 if (mActivity.isDestroyed()) {
828 // Loading item data is call from multiple AsyncTasks and the
829 // activity may be finished when buildItemFromData is called.
830 Log.d(TAG, "Activity destroyed, don't load data");
833 ImageData data = mDataAdapter.getImageData(dataID);
838 // Always scale by fixed filmstrip scale, since we only show items when
839 // in filmstrip. Preloading images with a different scale and bounds
840 // interferes with caching.
841 int width = Math.round(FILM_STRIP_SCALE * getWidth());
842 int height = Math.round(FILM_STRIP_SCALE * getHeight());
843 Log.v(TAG, "suggesting item bounds: " + width + "x" + height);
844 mDataAdapter.suggestViewSizeBound(width, height);
847 View recycled = getRecycledView(dataID);
848 View v = mDataAdapter.getView(mActivity.getAndroidContext(), recycled, dataID,
853 ViewItem item = new ViewItem(dataID, v, data);
854 item.addViewToHierarchy();
858 private void checkItemAtMaxSize() {
859 ViewItem item = mViewItem[mCurrentItem];
860 if (item.isMaximumBitmapRequested()) {
863 item.setMaximumBitmapRequested();
864 // Request full size bitmap, or max that DataAdapter will create.
865 int id = item.getId();
866 int h = mDataAdapter.getImageData(id).getHeight();
867 int w = mDataAdapter.getImageData(id).getWidth();
868 item.resizeView(mActivity, w, h);
871 private void removeItem(int itemID) {
872 if (itemID >= mViewItem.length || mViewItem[itemID] == null) {
875 ImageData data = mDataAdapter.getImageData(mViewItem[itemID].getId());
877 Log.e(TAG, "trying to remove a null item");
880 mViewItem[itemID].removeViewFromHierarchy(false);
881 mViewItem[itemID] = null;
885 * We try to keep the one closest to the center of the screen at position
888 private void stepIfNeeded() {
889 if (!inFilmstrip() && !inFullScreen()) {
890 // The good timing to step to the next view is when everything is
891 // not in transition.
894 final int nearest = findTheNearestView(mCenterX);
896 if (nearest == -1 || nearest == mCurrentItem) {
899 int prevDataId = (mViewItem[mCurrentItem] == null ? -1 : mViewItem[mCurrentItem].getId());
900 final int adjust = nearest - mCurrentItem;
902 for (int k = 0; k < adjust; k++) {
905 for (int k = 0; k + adjust < BUFFER_SIZE; k++) {
906 mViewItem[k] = mViewItem[k + adjust];
908 for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) {
910 if (mViewItem[k - 1] != null) {
911 mViewItem[k] = buildItemFromData(mViewItem[k - 1].getId() + 1);
916 for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) {
919 for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) {
920 mViewItem[k] = mViewItem[k + adjust];
922 for (int k = -1 - adjust; k >= 0; k--) {
924 if (mViewItem[k + 1] != null) {
925 mViewItem[k] = buildItemFromData(mViewItem[k + 1].getId() - 1);
930 if (mListener != null) {
931 mListener.onDataFocusChanged(prevDataId, mViewItem[mCurrentItem].getId());
932 final int firstVisible = mViewItem[mCurrentItem].getId() - 2;
933 final int visibleItemCount = firstVisible + BUFFER_SIZE;
934 final int totalItemCount = mDataAdapter.getTotalNumber();
935 mListener.onScroll(firstVisible, visibleItemCount, totalItemCount);
937 zoomAtIndexChanged();
941 * Check the bounds of {@code mCenterX}. Always call this function after: 1.
942 * Any changes to {@code mCenterX}. 2. Any size change of the view items.
944 * @return Whether clamp happened.
946 private boolean clampCenterX() {
947 ViewItem curr = mViewItem[mCurrentItem];
952 boolean stopScroll = false;
953 if (curr.getId() == 1 && mCenterX < curr.getCenterX() && mDataIdOnUserScrolling > 1 &&
954 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY &&
955 mController.isScrolling()) {
958 if (curr.getId() == 0 && mCenterX < curr.getCenterX()) {
959 // Stop at the first ViewItem.
963 if (curr.getId() == mDataAdapter.getTotalNumber() - 1
964 && mCenterX > curr.getCenterX()) {
970 mCenterX = curr.getCenterX();
977 * Reorders the child views to be consistent with their data ID. This method
978 * should be called after adding/removing views.
980 private void adjustChildZOrder() {
981 for (int i = BUFFER_SIZE - 1; i >= 0; i--) {
982 if (mViewItem[i] == null) {
985 mViewItem[i].bringViewToFront();
987 // ZoomView is a special case to always be in the front. In L set to
988 // max elevation to make sure ZoomView is above other elevated views.
989 bringChildToFront(mZoomView);
990 if (ApiHelper.isLOrHigher()) {
991 setMaxElevation(mZoomView);
995 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
996 private void setMaxElevation(View v) {
997 v.setElevation(Float.MAX_VALUE);
1001 * Returns the ID of the current item, or -1 if there is no data.
1003 private int getCurrentId() {
1004 ViewItem current = mViewItem[mCurrentItem];
1005 if (current == null) {
1008 return current.getId();
1012 * Keep the current item in the center. This functions does not check if the
1013 * current item is null.
1015 private void snapInCenter() {
1016 final ViewItem currItem = mViewItem[mCurrentItem];
1017 if (currItem == null) {
1020 final int currentViewCenter = currItem.getCenterX();
1021 if (mController.isScrolling() || mIsUserScrolling
1022 || isCurrentItemCentered()) {
1026 int snapInTime = (int) (SNAP_IN_CENTER_TIME_MS
1027 * ((float) Math.abs(mCenterX - currentViewCenter))
1028 / mDrawArea.width());
1029 mController.scrollToPosition(currentViewCenter,
1031 if (isViewTypeSticky(currItem) && !mController.isScaling() && mScale != FULL_SCREEN_SCALE) {
1032 // Now going to full screen camera
1033 mController.goToFullScreen();
1038 * Translates the {@link ViewItem} on the left of the current one to match
1039 * the full-screen layout. In full-screen, we show only one {@link ViewItem}
1040 * which occupies the whole screen. The other left ones are put on the left
1041 * side in full scales. Does nothing if there's no next item.
1043 * @param currItem The item ID of the current one to be translated.
1044 * @param drawAreaWidth The width of the current draw area.
1045 * @param scaleFraction A {@code float} between 0 and 1. 0 if the current
1046 * scale is {@code FILM_STRIP_SCALE}. 1 if the current scale is
1047 * {@code FULL_SCREEN_SCALE}.
1049 private void translateLeftViewItem(
1050 int currItem, int drawAreaWidth, float scaleFraction) {
1051 if (currItem < 0 || currItem > BUFFER_SIZE - 1) {
1052 Log.e(TAG, "currItem id out of bound.");
1056 final ViewItem curr = mViewItem[currItem];
1057 final ViewItem next = mViewItem[currItem + 1];
1058 if (curr == null || next == null) {
1059 Log.e(TAG, "Invalid view item (curr or next == null). curr = "
1064 final int currCenterX = curr.getCenterX();
1065 final int nextCenterX = next.getCenterX();
1066 final int translate = (int) ((nextCenterX - drawAreaWidth
1067 - currCenterX) * scaleFraction);
1069 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1071 curr.setVisibility(VISIBLE);
1073 if (inFullScreen()) {
1074 curr.setTranslationX(translate * (mCenterX - currCenterX) / (nextCenterX - currCenterX));
1076 curr.setTranslationX(translate);
1081 * Fade out the {@link ViewItem} on the right of the current one in
1082 * full-screen layout. Does nothing if there's no previous item.
1084 * @param currItemId The ID of the item to fade.
1086 private void fadeAndScaleRightViewItem(int currItemId) {
1087 if (currItemId < 1 || currItemId > BUFFER_SIZE) {
1088 Log.e(TAG, "currItem id out of bound.");
1092 final ViewItem currItem = mViewItem[currItemId];
1093 final ViewItem prevItem = mViewItem[currItemId - 1];
1094 if (currItem == null || prevItem == null) {
1095 Log.e(TAG, "Invalid view item (curr or prev == null). curr = "
1100 if (currItemId > mCurrentItem + 1) {
1101 // Every item not right next to the mCurrentItem is invisible.
1102 currItem.setVisibility(INVISIBLE);
1105 final int prevCenterX = prevItem.getCenterX();
1106 if (mCenterX <= prevCenterX) {
1107 // Shortcut. If the position is at the center of the previous one,
1108 // set to invisible too.
1109 currItem.setVisibility(INVISIBLE);
1112 final int currCenterX = currItem.getCenterX();
1113 final float fadeDownFraction =
1114 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX);
1115 currItem.layoutWithTranslationX(mDrawArea, currCenterX,
1116 FILM_STRIP_SCALE + (1f - FILM_STRIP_SCALE) * fadeDownFraction);
1117 currItem.setAlpha(fadeDownFraction);
1118 currItem.setTranslationX(0);
1119 currItem.setVisibility(VISIBLE);
1122 private void layoutViewItems(boolean layoutChanged) {
1123 if (mViewItem[mCurrentItem] == null ||
1124 mDrawArea.width() == 0 ||
1125 mDrawArea.height() == 0) {
1129 // If the layout changed, we need to adjust the current position so
1130 // that if an item is centered before the change, it's still centered.
1131 if (layoutChanged) {
1132 mViewItem[mCurrentItem].setLeftPosition(
1133 mCenterX - mViewItem[mCurrentItem].getMeasuredWidth() / 2);
1140 * Transformed scale fraction between 0 and 1. 0 if the scale is
1141 * {@link FILM_STRIP_SCALE}. 1 if the scale is {@link FULL_SCREEN_SCALE}
1144 final float scaleFraction = mViewAnimInterpolator.getInterpolation(
1145 (mScale - FILM_STRIP_SCALE) / (FULL_SCREEN_SCALE - FILM_STRIP_SCALE));
1146 final int fullScreenWidth = mDrawArea.width() + mViewGapInPixel;
1148 // Decide the position for all view items on the left and the right
1152 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
1153 final ViewItem curr = mViewItem[itemID];
1158 // First, layout relatively to the next one.
1159 final int currLeft = mViewItem[itemID + 1].getLeftPosition()
1160 - curr.getMeasuredWidth() - mViewGapInPixel;
1161 curr.setLeftPosition(currLeft);
1164 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
1165 final ViewItem curr = mViewItem[itemID];
1170 // First, layout relatively to the previous one.
1171 final ViewItem prev = mViewItem[itemID - 1];
1172 final int currLeft =
1173 prev.getLeftPosition() + prev.getMeasuredWidth()
1175 curr.setLeftPosition(currLeft);
1178 // Special case for the one immediately on the right of the camera
1180 boolean immediateRight =
1181 (mViewItem[mCurrentItem].getId() == 1 &&
1182 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY);
1184 // Layout the current ViewItem first.
1185 if (immediateRight) {
1186 // Just do a simple layout without any special translation or
1187 // fading. The implementation in Gallery does not push the first
1188 // photo to the bottom of the camera preview. Simply place the
1189 // photo on the right of the preview.
1190 final ViewItem currItem = mViewItem[mCurrentItem];
1191 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1192 currItem.setTranslationX(0f);
1193 currItem.setAlpha(1f);
1194 } else if (scaleFraction == 1f) {
1195 final ViewItem currItem = mViewItem[mCurrentItem];
1196 final int currCenterX = currItem.getCenterX();
1197 if (mCenterX < currCenterX) {
1198 // In full-screen and mCenterX is on the left of the center,
1199 // we draw the current one to "fade down".
1200 fadeAndScaleRightViewItem(mCurrentItem);
1201 } else if (mCenterX > currCenterX) {
1202 // In full-screen and mCenterX is on the right of the center,
1203 // we draw the current one translated.
1204 translateLeftViewItem(mCurrentItem, fullScreenWidth, scaleFraction);
1206 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1207 currItem.setTranslationX(0f);
1208 currItem.setAlpha(1f);
1211 final ViewItem currItem = mViewItem[mCurrentItem];
1212 // The normal filmstrip has no translation for the current item. If
1213 // it has translation before, gradually set it to zero.
1214 currItem.setTranslationX(currItem.getTranslationX() * scaleFraction);
1215 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1216 if (mViewItem[mCurrentItem - 1] == null) {
1217 currItem.setAlpha(1f);
1219 final int currCenterX = currItem.getCenterX();
1220 final int prevCenterX = mViewItem[mCurrentItem - 1].getCenterX();
1221 final float fadeDownFraction =
1222 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX);
1224 (1 - fadeDownFraction) * (1 - scaleFraction) + fadeDownFraction);
1228 // Layout the rest dependent on the current scale.
1230 // Items on the left
1231 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
1232 final ViewItem curr = mViewItem[itemID];
1236 translateLeftViewItem(itemID, fullScreenWidth, scaleFraction);
1239 // Items on the right
1240 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
1241 final ViewItem curr = mViewItem[itemID];
1246 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1247 if (curr.getId() == 1 && isViewTypeSticky(curr)) {
1248 // Special case for the one next to the camera preview.
1253 if (scaleFraction == 1) {
1254 // It's in full-screen mode.
1255 fadeAndScaleRightViewItem(itemID);
1257 boolean setToVisible = (curr.getVisibility() == INVISIBLE);
1259 if (itemID == mCurrentItem + 1) {
1260 curr.setAlpha(1f - scaleFraction);
1262 if (scaleFraction == 0f) {
1265 setToVisible = false;
1270 curr.setVisibility(VISIBLE);
1273 curr.setTranslationX(
1274 (mViewItem[mCurrentItem].getLeftPosition() - curr.getLeftPosition()) *
1282 private boolean isViewTypeSticky(ViewItem item) {
1286 return mDataAdapter.getImageData(item.getId()).getViewType() ==
1287 ImageData.VIEW_TYPE_STICKY;
1291 public void onDraw(Canvas c) {
1292 // TODO: remove layoutViewItems() here.
1293 layoutViewItems(false);
1298 protected void onLayout(boolean changed, int l, int t, int r, int b) {
1301 mDrawArea.right = r - l;
1302 mDrawArea.bottom = b - t;
1303 mZoomView.layout(mDrawArea.left, mDrawArea.top, mDrawArea.right, mDrawArea.bottom);
1304 // TODO: Need a more robust solution to decide when to re-layout
1305 // If in the middle of zooming, only re-layout when the layout has
1307 if (!inZoomView() || changed) {
1309 layoutViewItems(changed);
1314 * Clears the translation and scale that has been set on the view, cancels
1315 * any loading request for image partial decoding, and hides zoom view. This
1316 * is needed for when there is a layout change (e.g. when users re-enter the
1317 * app, or rotate the device, etc).
1319 private void resetZoomView() {
1320 if (!inZoomView()) {
1323 ViewItem current = mViewItem[mCurrentItem];
1324 if (current == null) {
1327 mScale = FULL_SCREEN_SCALE;
1328 mController.cancelZoomAnimation();
1329 mController.cancelFlingAnimation();
1330 current.resetTransform();
1331 mController.cancelLoadingZoomedImage();
1332 mZoomView.setVisibility(GONE);
1333 mController.setSurroundingViewsVisible(true);
1336 private void hideZoomView() {
1338 mController.cancelLoadingZoomedImage();
1339 mZoomView.setVisibility(GONE);
1343 private void slideViewBack(ViewItem item) {
1344 item.animateTranslationX(0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1345 item.animateTranslationY(0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1346 item.animateAlpha(1f, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1349 private void animateItemRemoval(int dataID, final ImageData data) {
1350 if (mScale > FULL_SCREEN_SCALE) {
1353 int removedItemId = findItemByDataID(dataID);
1355 // adjust the data id to be consistent
1356 for (int i = 0; i < BUFFER_SIZE; i++) {
1357 if (mViewItem[i] == null || mViewItem[i].getId() <= dataID) {
1360 mViewItem[i].setId(mViewItem[i].getId() - 1);
1362 if (removedItemId == -1) {
1366 final ViewItem removedItem = mViewItem[removedItemId];
1367 final int offsetX = removedItem.getMeasuredWidth() + mViewGapInPixel;
1369 for (int i = removedItemId + 1; i < BUFFER_SIZE; i++) {
1370 if (mViewItem[i] != null) {
1371 mViewItem[i].setLeftPosition(mViewItem[i].getLeftPosition() - offsetX);
1375 if (removedItemId >= mCurrentItem
1376 && mViewItem[removedItemId].getId() < mDataAdapter.getTotalNumber()) {
1377 // Fill the removed item by left shift when the current one or
1378 // anyone on the right is removed, and there's more data on the
1380 for (int i = removedItemId; i < BUFFER_SIZE - 1; i++) {
1381 mViewItem[i] = mViewItem[i + 1];
1384 // pull data out from the DataAdapter for the last one.
1385 int curr = BUFFER_SIZE - 1;
1386 int prev = curr - 1;
1387 if (mViewItem[prev] != null) {
1388 mViewItem[curr] = buildItemFromData(mViewItem[prev].getId() + 1);
1391 // The animation part.
1392 if (inFullScreen()) {
1393 mViewItem[mCurrentItem].setVisibility(VISIBLE);
1394 ViewItem nextItem = mViewItem[mCurrentItem + 1];
1395 if (nextItem != null) {
1396 nextItem.setVisibility(INVISIBLE);
1400 // Translate the views to their original places.
1401 for (int i = removedItemId; i < BUFFER_SIZE; i++) {
1402 if (mViewItem[i] != null) {
1403 mViewItem[i].setTranslationX(offsetX);
1407 // The end of the filmstrip might have been changed.
1408 // The mCenterX might be out of the bound.
1409 ViewItem currItem = mViewItem[mCurrentItem];
1410 if(currItem!=null) {
1411 if (currItem.getId() == mDataAdapter.getTotalNumber() - 1
1412 && mCenterX > currItem.getCenterX()) {
1413 int adjustDiff = currItem.getCenterX() - mCenterX;
1414 mCenterX = currItem.getCenterX();
1415 for (int i = 0; i < BUFFER_SIZE; i++) {
1416 if (mViewItem[i] != null) {
1417 mViewItem[i].translateXScaledBy(adjustDiff);
1422 // CurrItem should NOT be NULL, but if is, at least don't crash.
1423 Log.w(TAG,"Caught invalid update in removal animation.");
1426 // fill the removed place by right shift
1427 mCenterX -= offsetX;
1429 for (int i = removedItemId; i > 0; i--) {
1430 mViewItem[i] = mViewItem[i - 1];
1433 // pull data out from the DataAdapter for the first one.
1435 int next = curr + 1;
1436 if (mViewItem[next] != null) {
1437 mViewItem[curr] = buildItemFromData(mViewItem[next].getId() - 1);
1440 // Translate the views to their original places.
1441 for (int i = removedItemId; i >= 0; i--) {
1442 if (mViewItem[i] != null) {
1443 mViewItem[i].setTranslationX(-offsetX);
1448 int transY = getHeight() / 8;
1449 if (removedItem.getTranslationY() < 0) {
1452 removedItem.animateTranslationY(removedItem.getTranslationY() + transY,
1453 GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1454 removedItem.animateAlpha(0f, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1455 postDelayed(new Runnable() {
1458 removedItem.removeViewFromHierarchy(false);
1460 }, GEOMETRY_ADJUST_TIME_MS);
1462 adjustChildZOrder();
1465 // Now, slide every one back.
1466 if (mViewItem[mCurrentItem] == null) {
1469 for (int i = 0; i < BUFFER_SIZE; i++) {
1470 if (mViewItem[i] != null
1471 && mViewItem[i].getTranslationX() != 0f) {
1472 slideViewBack(mViewItem[i]);
1475 if (isCurrentItemCentered() && isViewTypeSticky(mViewItem[mCurrentItem])) {
1476 // Special case for scrolling onto the camera preview after removal.
1477 mController.goToFullScreen();
1481 // returns -1 on failure.
1482 private int findItemByDataID(int dataID) {
1483 for (int i = 0; i < BUFFER_SIZE; i++) {
1484 if (mViewItem[i] != null
1485 && mViewItem[i].getId() == dataID) {
1492 private void updateInsertion(int dataID) {
1493 int insertedItemId = findItemByDataID(dataID);
1494 if (insertedItemId == -1) {
1495 // Not in the current item buffers. Check if it's inserted
1497 if (dataID == mDataAdapter.getTotalNumber() - 1) {
1498 int prev = findItemByDataID(dataID - 1);
1499 if (prev >= 0 && prev < BUFFER_SIZE - 1) {
1500 // The previous data is in the buffer and we still
1501 // have room for the inserted data.
1502 insertedItemId = prev + 1;
1507 // adjust the data id to be consistent
1508 for (int i = 0; i < BUFFER_SIZE; i++) {
1509 if (mViewItem[i] == null || mViewItem[i].getId() < dataID) {
1512 mViewItem[i].setId(mViewItem[i].getId() + 1);
1514 if (insertedItemId == -1) {
1518 final ImageData data = mDataAdapter.getImageData(dataID);
1519 Point dim = CameraUtil
1520 .resizeToFill(data.getWidth(), data.getHeight(), data.getRotation(),
1521 getMeasuredWidth(), getMeasuredHeight());
1522 final int offsetX = dim.x + mViewGapInPixel;
1523 ViewItem viewItem = buildItemFromData(dataID);
1524 if (viewItem == null) {
1525 Log.w(TAG, "unable to build inserted item from data");
1529 if (insertedItemId >= mCurrentItem) {
1530 if (insertedItemId == mCurrentItem) {
1531 viewItem.setLeftPosition(mViewItem[mCurrentItem].getLeftPosition());
1533 // Shift right to make rooms for newly inserted item.
1534 removeItem(BUFFER_SIZE - 1);
1535 for (int i = BUFFER_SIZE - 1; i > insertedItemId; i--) {
1536 mViewItem[i] = mViewItem[i - 1];
1537 if (mViewItem[i] != null) {
1538 mViewItem[i].setTranslationX(-offsetX);
1539 slideViewBack(mViewItem[i]);
1543 // Shift left. Put the inserted data on the left instead of the
1546 if (insertedItemId < 0) {
1550 for (int i = 1; i <= insertedItemId; i++) {
1551 if (mViewItem[i] != null) {
1552 mViewItem[i].setTranslationX(offsetX);
1553 slideViewBack(mViewItem[i]);
1554 mViewItem[i - 1] = mViewItem[i];
1559 mViewItem[insertedItemId] = viewItem;
1560 viewItem.setAlpha(0f);
1561 viewItem.setTranslationY(getHeight() / 8);
1562 slideViewBack(viewItem);
1563 adjustChildZOrder();
1567 private void setDataAdapter(DataAdapter adapter) {
1568 mDataAdapter = adapter;
1569 int maxEdge = (int) (Math.max(this.getHeight(), this.getWidth())
1570 * FILM_STRIP_SCALE);
1571 mDataAdapter.suggestViewSizeBound(maxEdge, maxEdge);
1572 mDataAdapter.setListener(new DataAdapter.Listener() {
1574 public void onDataLoaded() {
1579 public void onDataUpdated(DataAdapter.UpdateReporter reporter) {
1584 public void onDataInserted(int dataId, ImageData data) {
1585 if (mViewItem[mCurrentItem] == null) {
1586 // empty now, simply do a reload.
1589 updateInsertion(dataId);
1591 if (mListener != null) {
1592 mListener.onDataFocusChanged(dataId, getCurrentId());
1597 public void onDataRemoved(int dataId, ImageData data) {
1598 animateItemRemoval(dataId, data);
1599 if (mListener != null) {
1600 mListener.onDataFocusChanged(dataId, getCurrentId());
1606 private boolean inFilmstrip() {
1607 return (mScale == FILM_STRIP_SCALE);
1610 private boolean inFullScreen() {
1611 return (mScale == FULL_SCREEN_SCALE);
1614 private boolean inZoomView() {
1615 return (mScale > FULL_SCREEN_SCALE);
1618 private boolean isCameraPreview() {
1619 return isViewTypeSticky(mViewItem[mCurrentItem]);
1622 private boolean inCameraFullscreen() {
1623 return isDataAtCenter(0) && inFullScreen()
1624 && (isViewTypeSticky(mViewItem[mCurrentItem]));
1628 public boolean onInterceptTouchEvent(MotionEvent ev) {
1629 if (mController.isScrolling()) {
1633 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
1634 mCheckToIntercept = true;
1635 mDown = MotionEvent.obtain(ev);
1636 ViewItem viewItem = mViewItem[mCurrentItem];
1637 // Do not intercept touch if swipe is not enabled
1638 if (viewItem != null && !mDataAdapter.canSwipeInFullScreen(viewItem.getId())) {
1639 mCheckToIntercept = false;
1642 } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
1643 // Do not intercept touch once child is in zoom mode
1644 mCheckToIntercept = false;
1647 if (!mCheckToIntercept) {
1650 if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) {
1653 int deltaX = (int) (ev.getX() - mDown.getX());
1654 int deltaY = (int) (ev.getY() - mDown.getY());
1655 if (ev.getActionMasked() == MotionEvent.ACTION_MOVE
1656 && deltaX < mSlop * (-1)) {
1657 // intercept left swipe
1658 if (Math.abs(deltaX) >= Math.abs(deltaY) * 2) {
1667 public boolean onTouchEvent(MotionEvent ev) {
1668 return mGestureRecognizer.onTouchEvent(ev);
1671 FilmstripGestureRecognizer.Listener getGestureListener() {
1672 return mGestureListener;
1675 private void updateViewItem(int itemID) {
1676 ViewItem item = mViewItem[itemID];
1678 Log.e(TAG, "trying to update an null item");
1681 item.removeViewFromHierarchy(true);
1683 ViewItem newItem = buildItemFromData(item.getId());
1684 if (newItem == null) {
1685 Log.e(TAG, "new item is null");
1686 // keep using the old data.
1687 item.addViewToHierarchy();
1690 newItem.copyAttributes(item);
1691 mViewItem[itemID] = newItem;
1692 mZoomView.resetDecoder();
1694 boolean stopScroll = clampCenterX();
1696 mController.stopScrolling(true);
1698 adjustChildZOrder();
1700 if (mListener != null) {
1701 mListener.onDataUpdated(newItem.getId());
1705 /** Some of the data is changed. */
1706 private void update(DataAdapter.UpdateReporter reporter) {
1708 if (mViewItem[mCurrentItem] == null) {
1713 // Check the current one.
1714 ViewItem curr = mViewItem[mCurrentItem];
1715 int dataId = curr.getId();
1716 if (reporter.isDataRemoved(dataId)) {
1720 if (reporter.isDataUpdated(dataId)) {
1721 updateViewItem(mCurrentItem);
1722 final ImageData data = mDataAdapter.getImageData(dataId);
1723 if (!mIsUserScrolling && !mController.isScrolling()) {
1724 // If there is no scrolling at all, adjust mCenterX to place
1725 // the current item at the center.
1726 Point dim = CameraUtil.resizeToFill(data.getWidth(), data.getHeight(),
1727 data.getRotation(), getMeasuredWidth(), getMeasuredHeight());
1728 mCenterX = curr.getLeftPosition() + dim.x / 2;
1733 for (int i = mCurrentItem - 1; i >= 0; i--) {
1734 curr = mViewItem[i];
1736 dataId = curr.getId();
1737 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
1741 ViewItem next = mViewItem[i + 1];
1743 mViewItem[i] = buildItemFromData(next.getId() - 1);
1749 for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) {
1750 curr = mViewItem[i];
1752 dataId = curr.getId();
1753 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
1757 ViewItem prev = mViewItem[i - 1];
1759 mViewItem[i] = buildItemFromData(prev.getId() + 1);
1763 adjustChildZOrder();
1764 // Request a layout to find the measured width/height of the view first.
1766 // Update photo sphere visibility after metadata fully written.
1770 * The whole data might be totally different. Flush all and load from the
1771 * start. Filmstrip will be centered on the first item, i.e. the camera
1774 private void reload() {
1775 mController.stopScrolling(true);
1776 mController.stopScale();
1777 mDataIdOnUserScrolling = 0;
1780 if (mViewItem[mCurrentItem] != null) {
1781 prevId = mViewItem[mCurrentItem].getId();
1784 // Remove all views from the mViewItem buffer, except the camera view.
1785 for (int i = 0; i < mViewItem.length; i++) {
1786 if (mViewItem[i] == null) {
1789 mViewItem[i].removeViewFromHierarchy(false);
1792 // Clear out the mViewItems and rebuild with camera in the center.
1793 Arrays.fill(mViewItem, null);
1794 int dataNumber = mDataAdapter.getTotalNumber();
1795 if (dataNumber == 0) {
1799 mViewItem[mCurrentItem] = buildItemFromData(0);
1800 if (mViewItem[mCurrentItem] == null) {
1803 mViewItem[mCurrentItem].setLeftPosition(0);
1804 for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) {
1805 mViewItem[i] = buildItemFromData(mViewItem[i - 1].getId() + 1);
1806 if (mViewItem[i] == null) {
1811 // Ensure that the views in mViewItem will layout the first in the
1812 // center of the display upon a reload.
1814 mScale = FILM_STRIP_SCALE;
1816 adjustChildZOrder();
1819 if (mListener != null) {
1820 mListener.onDataReloaded();
1821 mListener.onDataFocusChanged(prevId, mViewItem[mCurrentItem].getId());
1825 private void promoteData(int itemID, int dataID) {
1826 if (mListener != null) {
1827 mListener.onFocusedDataPromoted(dataID);
1831 private void demoteData(int itemID, int dataID) {
1832 if (mListener != null) {
1833 mListener.onFocusedDataDemoted(dataID);
1837 private void onEnterFilmstrip() {
1838 if (mListener != null) {
1839 mListener.onEnterFilmstrip(getCurrentId());
1843 private void onLeaveFilmstrip() {
1844 if (mListener != null) {
1845 mListener.onLeaveFilmstrip(getCurrentId());
1849 private void onEnterFullScreen() {
1850 mFullScreenUIHidden = false;
1851 if (mListener != null) {
1852 mListener.onEnterFullScreenUiShown(getCurrentId());
1856 private void onLeaveFullScreen() {
1857 if (mListener != null) {
1858 mListener.onLeaveFullScreenUiShown(getCurrentId());
1862 private void onEnterFullScreenUiHidden() {
1863 mFullScreenUIHidden = true;
1864 if (mListener != null) {
1865 mListener.onEnterFullScreenUiHidden(getCurrentId());
1869 private void onLeaveFullScreenUiHidden() {
1870 mFullScreenUIHidden = false;
1871 if (mListener != null) {
1872 mListener.onLeaveFullScreenUiHidden(getCurrentId());
1876 private void onEnterZoomView() {
1877 if (mListener != null) {
1878 mListener.onEnterZoomView(getCurrentId());
1882 private void onLeaveZoomView() {
1883 mController.setSurroundingViewsVisible(true);
1887 * MyController controls all the geometry animations. It passively tells the
1888 * geometry information on demand.
1890 private class MyController implements FilmstripController {
1892 private final ValueAnimator mScaleAnimator;
1893 private ValueAnimator mZoomAnimator;
1894 private AnimatorSet mFlingAnimator;
1896 private final MyScroller mScroller;
1897 private boolean mCanStopScroll;
1899 private final MyScroller.Listener mScrollerListener =
1900 new MyScroller.Listener() {
1902 public void onScrollUpdate(int currX, int currY) {
1905 boolean stopScroll = clampCenterX();
1907 mController.stopScrolling(true);
1913 public void onScrollEnd() {
1914 mCanStopScroll = true;
1915 if (mViewItem[mCurrentItem] == null) {
1919 if (isCurrentItemCentered()
1920 && isViewTypeSticky(mViewItem[mCurrentItem])) {
1921 // Special case for the scrolling end on the camera
1928 private final ValueAnimator.AnimatorUpdateListener mScaleAnimatorUpdateListener =
1929 new ValueAnimator.AnimatorUpdateListener() {
1931 public void onAnimationUpdate(ValueAnimator animation) {
1932 if (mViewItem[mCurrentItem] == null) {
1935 mScale = (Float) animation.getAnimatedValue();
1940 MyController(Context context) {
1941 TimeInterpolator decelerateInterpolator = new DecelerateInterpolator(1.5f);
1942 mScroller = new MyScroller(mActivity.getAndroidContext(),
1943 new Handler(mActivity.getMainLooper()),
1944 mScrollerListener, decelerateInterpolator);
1945 mCanStopScroll = true;
1947 mScaleAnimator = new ValueAnimator();
1948 mScaleAnimator.addUpdateListener(mScaleAnimatorUpdateListener);
1949 mScaleAnimator.setInterpolator(decelerateInterpolator);
1950 mScaleAnimator.addListener(new Animator.AnimatorListener() {
1952 public void onAnimationStart(Animator animator) {
1953 if (mScale == FULL_SCREEN_SCALE) {
1954 onLeaveFullScreen();
1956 if (mScale == FILM_STRIP_SCALE) {
1963 public void onAnimationEnd(Animator animator) {
1964 if (mScale == FULL_SCREEN_SCALE) {
1965 onEnterFullScreen();
1967 if (mScale == FILM_STRIP_SCALE) {
1971 zoomAtIndexChanged();
1975 public void onAnimationCancel(Animator animator) {
1980 public void onAnimationRepeat(Animator animator) {
1987 public void setImageGap(int imageGap) {
1988 FilmstripView.this.setViewGap(imageGap);
1992 public int getCurrentId() {
1993 return FilmstripView.this.getCurrentId();
1997 public void setDataAdapter(DataAdapter adapter) {
1998 FilmstripView.this.setDataAdapter(adapter);
2002 public boolean inFilmstrip() {
2003 return FilmstripView.this.inFilmstrip();
2007 public boolean inFullScreen() {
2008 return FilmstripView.this.inFullScreen();
2012 public boolean isCameraPreview() {
2013 return FilmstripView.this.isCameraPreview();
2017 public boolean inCameraFullscreen() {
2018 return FilmstripView.this.inCameraFullscreen();
2022 public void setListener(FilmstripListener l) {
2023 FilmstripView.this.setListener(l);
2027 public boolean isScrolling() {
2028 return !mScroller.isFinished();
2032 public boolean isScaling() {
2033 return mScaleAnimator.isRunning();
2036 private int estimateMinX(int dataID, int leftPos, int viewWidth) {
2037 return leftPos - (dataID + 100) * (viewWidth + mViewGapInPixel);
2040 private int estimateMaxX(int dataID, int leftPos, int viewWidth) {
2042 + (mDataAdapter.getTotalNumber() - dataID + 100)
2043 * (viewWidth + mViewGapInPixel);
2046 /** Zoom all the way in or out on the image at the given pivot point. */
2047 private void zoomAt(final ViewItem current, final float focusX, final float focusY) {
2048 // End previous zoom animation, if any
2049 if (mZoomAnimator != null) {
2050 mZoomAnimator.end();
2052 // Calculate end scale
2053 final float maxScale = getCurrentDataMaxScale(false);
2054 final float endScale = mScale < maxScale - maxScale * TOLERANCE
2055 ? maxScale : FULL_SCREEN_SCALE;
2057 mZoomAnimator = new ValueAnimator();
2058 mZoomAnimator.setFloatValues(mScale, endScale);
2059 mZoomAnimator.setDuration(ZOOM_ANIMATION_DURATION_MS);
2060 mZoomAnimator.addListener(new Animator.AnimatorListener() {
2062 public void onAnimationStart(Animator animation) {
2063 if (mScale == FULL_SCREEN_SCALE) {
2064 if (mFullScreenUIHidden) {
2065 onLeaveFullScreenUiHidden();
2067 onLeaveFullScreen();
2069 setSurroundingViewsVisible(false);
2070 } else if (inZoomView()) {
2073 cancelLoadingZoomedImage();
2077 public void onAnimationEnd(Animator animation) {
2078 // Make sure animation ends up having the correct scale even
2079 // if it is cancelled before it finishes
2080 if (mScale != endScale) {
2081 current.postScale(focusX, focusY, endScale / mScale, mDrawArea.width(),
2082 mDrawArea.height());
2086 if (inFullScreen()) {
2087 setSurroundingViewsVisible(true);
2088 mZoomView.setVisibility(GONE);
2089 current.resetTransform();
2090 onEnterFullScreenUiHidden();
2092 mController.loadZoomedImage();
2095 mZoomAnimator = null;
2096 zoomAtIndexChanged();
2100 public void onAnimationCancel(Animator animation) {
2105 public void onAnimationRepeat(Animator animation) {
2110 mZoomAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2112 public void onAnimationUpdate(ValueAnimator animation) {
2113 float newScale = (Float) animation.getAnimatedValue();
2114 float postScale = newScale / mScale;
2116 current.postScale(focusX, focusY, postScale, mDrawArea.width(),
2117 mDrawArea.height());
2120 mZoomAnimator.start();
2124 public void scroll(float deltaX) {
2125 if (!stopScrolling(false)) {
2130 boolean stopScroll = clampCenterX();
2132 mController.stopScrolling(true);
2138 public void fling(float velocityX) {
2139 if (!stopScrolling(false)) {
2142 final ViewItem item = mViewItem[mCurrentItem];
2147 float scaledVelocityX = velocityX / mScale;
2148 if (inFullScreen() && isViewTypeSticky(item) && scaledVelocityX < 0) {
2149 // Swipe left in camera preview.
2154 // Estimation of possible length on the left. To ensure the
2155 // velocity doesn't become too slow eventually, we add a huge number
2156 // to the estimated maximum.
2157 int minX = estimateMinX(item.getId(), item.getLeftPosition(), w);
2158 // Estimation of possible length on the right. Likewise, exaggerate
2159 // the possible maximum too.
2160 int maxX = estimateMaxX(item.getId(), item.getLeftPosition(), w);
2161 mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0);
2164 void flingInsideZoomView(float velocityX, float velocityY) {
2165 if (!inZoomView()) {
2169 final ViewItem current = mViewItem[mCurrentItem];
2170 if (current == null) {
2174 final int factor = DECELERATION_FACTOR;
2175 // Deceleration curve for distance:
2176 // S(t) = s + (e - s) * (1 - (1 - t/T) ^ factor)
2177 // Need to find the ending distance (e), so that the starting
2178 // velocity is the velocity of fling.
2179 // Velocity is the derivative of distance
2180 // V(t) = (e - s) * factor * (-1) * (1 - t/T) ^ (factor - 1) * (-1/T)
2181 // = (e - s) * factor * (1 - t/T) ^ (factor - 1) / T
2182 // Since V(0) = V0, we have e = T / factor * V0 + s
2184 // Duration T should be long enough so that at the end of the fling,
2185 // image moves at 1 pixel/s for about P = 50ms = 0.05s
2186 // i.e. V(T - P) = 1
2187 // V(T - P) = V0 * (1 - (T -P) /T) ^ (factor - 1) = 1
2188 // T = P * V0 ^ (1 / (factor -1))
2190 final float velocity = Math.max(Math.abs(velocityX), Math.abs(velocityY));
2191 // Dynamically calculate duration
2192 final float duration = (float) (FLING_COASTING_DURATION_S
2193 * Math.pow(velocity, (1f / (factor - 1f))));
2195 final float translationX = current.getTranslationX() * mScale;
2196 final float translationY = current.getTranslationY() * mScale;
2198 final ValueAnimator decelerationX = ValueAnimator.ofFloat(translationX,
2199 translationX + duration / factor * velocityX);
2200 final ValueAnimator decelerationY = ValueAnimator.ofFloat(translationY,
2201 translationY + duration / factor * velocityY);
2203 decelerationY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2205 public void onAnimationUpdate(ValueAnimator animation) {
2206 float transX = (Float) decelerationX.getAnimatedValue();
2207 float transY = (Float) decelerationY.getAnimatedValue();
2209 current.updateTransform(transX, transY, mScale,
2210 mScale, mDrawArea.width(), mDrawArea.height());
2214 mFlingAnimator = new AnimatorSet();
2215 mFlingAnimator.play(decelerationX).with(decelerationY);
2216 mFlingAnimator.setDuration((int) (duration * 1000));
2217 mFlingAnimator.setInterpolator(new TimeInterpolator() {
2219 public float getInterpolation(float input) {
2220 return (float) (1.0f - Math.pow((1.0f - input), factor));
2223 mFlingAnimator.addListener(new Animator.AnimatorListener() {
2224 private boolean mCancelled = false;
2227 public void onAnimationStart(Animator animation) {
2232 public void onAnimationEnd(Animator animation) {
2236 mFlingAnimator = null;
2240 public void onAnimationCancel(Animator animation) {
2245 public void onAnimationRepeat(Animator animation) {
2249 mFlingAnimator.start();
2253 public boolean stopScrolling(boolean forced) {
2254 if (!isScrolling()) {
2256 } else if (!mCanStopScroll && !forced) {
2259 mScroller.forceFinished(true);
2263 private void stopScale() {
2264 mScaleAnimator.cancel();
2268 public void scrollToPosition(int position, int duration, boolean interruptible) {
2269 if (mViewItem[mCurrentItem] == null) {
2272 mCanStopScroll = interruptible;
2273 mScroller.startScroll(mCenterX, 0, position - mCenterX, 0, duration);
2277 public boolean goToNextItem() {
2278 return goToItem(mCurrentItem + 1);
2282 public boolean goToPreviousItem() {
2283 return goToItem(mCurrentItem - 1);
2286 private boolean goToItem(int itemIndex) {
2287 final ViewItem nextItem = mViewItem[itemIndex];
2288 if (nextItem == null) {
2291 stopScrolling(true);
2292 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS * 2, false);
2294 if (isViewTypeSticky(mViewItem[mCurrentItem])) {
2295 // Special case when moving from camera preview.
2296 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
2301 private void scaleTo(float scale, int duration) {
2302 if (mViewItem[mCurrentItem] == null) {
2306 mScaleAnimator.setDuration(duration);
2307 mScaleAnimator.setFloatValues(mScale, scale);
2308 mScaleAnimator.start();
2312 public void goToFilmstrip() {
2313 if (mViewItem[mCurrentItem] == null) {
2316 if (mScale == FILM_STRIP_SCALE) {
2319 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
2321 final ViewItem currItem = mViewItem[mCurrentItem];
2322 final ViewItem nextItem = mViewItem[mCurrentItem + 1];
2323 if (currItem.getId() == 0 && isViewTypeSticky(currItem) && nextItem != null) {
2324 // Deal with the special case of swiping in camera preview.
2325 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, false);
2328 if (mScale == FILM_STRIP_SCALE) {
2334 public void goToFullScreen() {
2335 if (inFullScreen()) {
2339 scaleTo(FULL_SCREEN_SCALE, GEOMETRY_ADJUST_TIME_MS);
2342 private void cancelFlingAnimation() {
2343 // Cancels flinging for zoomed images
2344 if (isFlingAnimationRunning()) {
2345 mFlingAnimator.cancel();
2349 private void cancelZoomAnimation() {
2350 if (isZoomAnimationRunning()) {
2351 mZoomAnimator.cancel();
2355 private void setSurroundingViewsVisible(boolean visible) {
2356 // Hide everything on the left
2357 // TODO: Need to find a better way to toggle the visibility of views
2358 // around the current view.
2359 for (int i = 0; i < mCurrentItem; i++) {
2360 if (i == mCurrentItem || mViewItem[i] == null) {
2363 mViewItem[i].setVisibility(visible ? VISIBLE : INVISIBLE);
2367 private Uri getCurrentUri() {
2368 ViewItem curr = mViewItem[mCurrentItem];
2372 return mDataAdapter.getImageData(curr.getId()).getUri();
2376 * Here we only support up to 1:1 image zoom (i.e. a 100% view of the
2377 * actual pixels). The max scale that we can apply on the view should
2378 * make the view same size as the image, in pixels.
2380 private float getCurrentDataMaxScale(boolean allowOverScale) {
2381 ViewItem curr = mViewItem[mCurrentItem];
2383 return FULL_SCREEN_SCALE;
2385 ImageData imageData = mDataAdapter.getImageData(curr.getId());
2386 if (imageData == null || !imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) {
2387 return FULL_SCREEN_SCALE;
2389 float imageWidth = imageData.getWidth();
2390 if (imageData.getRotation() == 90
2391 || imageData.getRotation() == 270) {
2392 imageWidth = imageData.getHeight();
2394 float scale = imageWidth / curr.getWidth();
2395 if (allowOverScale) {
2396 // In addition to the scale we apply to the view for 100% view
2397 // (i.e. each pixel on screen corresponds to a pixel in image)
2398 // we allow scaling beyond that for better detail viewing.
2399 scale *= mOverScaleFactor;
2404 private void loadZoomedImage() {
2405 if (!inZoomView()) {
2408 ViewItem curr = mViewItem[mCurrentItem];
2412 ImageData imageData = mDataAdapter.getImageData(curr.getId());
2413 if (!imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) {
2416 Uri uri = getCurrentUri();
2417 RectF viewRect = curr.getViewRect();
2418 if (uri == null || uri == Uri.EMPTY) {
2421 int orientation = imageData.getRotation();
2422 mZoomView.loadBitmap(uri, orientation, viewRect);
2425 private void cancelLoadingZoomedImage() {
2426 mZoomView.cancelPartialDecodingTask();
2430 public void goToFirstItem() {
2431 if (mViewItem[mCurrentItem] == null) {
2435 // TODO: animate to camera if it is still in the mViewItem buffer
2436 // versus a full reload which will perform an immediate transition
2440 public boolean inZoomView() {
2441 return FilmstripView.this.inZoomView();
2444 public boolean isFlingAnimationRunning() {
2445 return mFlingAnimator != null && mFlingAnimator.isRunning();
2448 public boolean isZoomAnimationRunning() {
2449 return mZoomAnimator != null && mZoomAnimator.isRunning();
2453 private boolean isCurrentItemCentered() {
2454 return mViewItem[mCurrentItem].getCenterX() == mCenterX;
2457 private static class MyScroller {
2458 public interface Listener {
2459 public void onScrollUpdate(int currX, int currY);
2461 public void onScrollEnd();
2464 private final Handler mHandler;
2465 private final Listener mListener;
2467 private final Scroller mScroller;
2469 private final ValueAnimator mXScrollAnimator;
2470 private final Runnable mScrollChecker = new Runnable() {
2473 boolean newPosition = mScroller.computeScrollOffset();
2475 mListener.onScrollEnd();
2478 mListener.onScrollUpdate(mScroller.getCurrX(), mScroller.getCurrY());
2479 mHandler.removeCallbacks(this);
2480 mHandler.post(this);
2484 private final ValueAnimator.AnimatorUpdateListener mXScrollAnimatorUpdateListener =
2485 new ValueAnimator.AnimatorUpdateListener() {
2487 public void onAnimationUpdate(ValueAnimator animation) {
2488 mListener.onScrollUpdate((Integer) animation.getAnimatedValue(), 0);
2492 private final Animator.AnimatorListener mXScrollAnimatorListener =
2493 new Animator.AnimatorListener() {
2495 public void onAnimationCancel(Animator animation) {
2500 public void onAnimationEnd(Animator animation) {
2501 mListener.onScrollEnd();
2505 public void onAnimationRepeat(Animator animation) {
2510 public void onAnimationStart(Animator animation) {
2515 public MyScroller(Context ctx, Handler handler, Listener listener,
2516 TimeInterpolator interpolator) {
2518 mListener = listener;
2519 mScroller = new Scroller(ctx);
2520 mXScrollAnimator = new ValueAnimator();
2521 mXScrollAnimator.addUpdateListener(mXScrollAnimatorUpdateListener);
2522 mXScrollAnimator.addListener(mXScrollAnimatorListener);
2523 mXScrollAnimator.setInterpolator(interpolator);
2527 int startX, int startY,
2528 int velocityX, int velocityY,
2530 int minY, int maxY) {
2531 mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
2535 public void startScroll(int startX, int startY, int dx, int dy) {
2536 mScroller.startScroll(startX, startY, dx, dy);
2540 /** Only starts and updates scroll in x-axis. */
2541 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
2542 mXScrollAnimator.cancel();
2543 mXScrollAnimator.setDuration(duration);
2544 mXScrollAnimator.setIntValues(startX, startX + dx);
2545 mXScrollAnimator.start();
2548 public boolean isFinished() {
2549 return (mScroller.isFinished() && !mXScrollAnimator.isRunning());
2552 public void forceFinished(boolean finished) {
2553 mScroller.forceFinished(finished);
2555 mXScrollAnimator.cancel();
2559 private void runChecker() {
2560 if (mHandler == null || mListener == null) {
2563 mHandler.removeCallbacks(mScrollChecker);
2564 mHandler.post(mScrollChecker);
2568 private class MyGestureReceiver implements FilmstripGestureRecognizer.Listener {
2570 private static final int SCROLL_DIR_NONE = 0;
2571 private static final int SCROLL_DIR_VERTICAL = 1;
2572 private static final int SCROLL_DIR_HORIZONTAL = 2;
2573 // Indicating the current trend of scaling is up (>1) or down (<1).
2574 private float mScaleTrend;
2575 private float mMaxScale;
2576 private int mScrollingDirection = SCROLL_DIR_NONE;
2577 private long mLastDownTime;
2578 private float mLastDownY;
2581 public boolean onSingleTapUp(float x, float y) {
2582 ViewItem centerItem = mViewItem[mCurrentItem];
2583 if (inFilmstrip()) {
2584 if (centerItem != null && centerItem.areaContains(x, y)) {
2585 mController.goToFullScreen();
2588 } else if (inFullScreen()) {
2589 if (mFullScreenUIHidden) {
2590 onLeaveFullScreenUiHidden();
2591 onEnterFullScreen();
2593 onLeaveFullScreen();
2594 onEnterFullScreenUiHidden();
2602 public boolean onDoubleTap(float x, float y) {
2603 ViewItem current = mViewItem[mCurrentItem];
2604 if (current == null) {
2607 if (inFilmstrip()) {
2608 mController.goToFullScreen();
2610 } else if (mScale < FULL_SCREEN_SCALE || inCameraFullscreen()) {
2613 if (!mController.stopScrolling(false)) {
2616 if (inFullScreen()) {
2617 mController.zoomAt(current, x, y);
2618 checkItemAtMaxSize();
2620 } else if (mScale > FULL_SCREEN_SCALE) {
2622 mController.zoomAt(current, x, y);
2628 public boolean onDown(float x, float y) {
2629 mLastDownTime = SystemClock.uptimeMillis();
2631 mController.cancelFlingAnimation();
2632 if (!mController.stopScrolling(false)) {
2640 public boolean onUp(float x, float y) {
2641 ViewItem currItem = mViewItem[mCurrentItem];
2642 if (currItem == null) {
2645 if (mController.isZoomAnimationRunning() || mController.isFlingAnimationRunning()) {
2649 mController.loadZoomedImage();
2652 float promoteHeight = getHeight() * PROMOTE_HEIGHT_RATIO;
2653 float velocityPromoteHeight = getHeight() * VELOCITY_PROMOTE_HEIGHT_RATIO;
2654 mIsUserScrolling = false;
2655 mScrollingDirection = SCROLL_DIR_NONE;
2656 // Finds items promoted/demoted.
2657 float speedY = Math.abs(y - mLastDownY)
2658 / (SystemClock.uptimeMillis() - mLastDownTime);
2659 for (int i = 0; i < BUFFER_SIZE; i++) {
2660 if (mViewItem[i] == null) {
2663 float transY = mViewItem[i].getTranslationY();
2667 int id = mViewItem[i].getId();
2669 if (mDataAdapter.getImageData(id)
2670 .isUIActionSupported(ImageData.ACTION_DEMOTE)
2671 && ((transY > promoteHeight)
2672 || (transY > velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) {
2674 } else if (mDataAdapter.getImageData(id)
2675 .isUIActionSupported(ImageData.ACTION_PROMOTE)
2676 && (transY < -promoteHeight
2677 || (transY < -velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) {
2680 // put the view back.
2681 slideViewBack(mViewItem[i]);
2685 // The data might be changed. Re-check.
2686 currItem = mViewItem[mCurrentItem];
2687 if (currItem == null) {
2691 int currId = currItem.getId();
2692 if (mCenterX > currItem.getCenterX() + CAMERA_PREVIEW_SWIPE_THRESHOLD && currId == 0 &&
2693 isViewTypeSticky(currItem) && mDataIdOnUserScrolling == 0) {
2694 mController.goToFilmstrip();
2695 // Special case to go from camera preview to the next photo.
2696 if (mViewItem[mCurrentItem + 1] != null) {
2697 mController.scrollToPosition(
2698 mViewItem[mCurrentItem + 1].getCenterX(),
2699 GEOMETRY_ADJUST_TIME_MS, false);
2705 if (isCurrentItemCentered() && currId == 0 && isViewTypeSticky(currItem)) {
2706 mController.goToFullScreen();
2708 if (mDataIdOnUserScrolling == 0 && currId != 0) {
2709 // Special case to go to filmstrip when the user scroll away
2710 // from the camera preview and the current one is not the
2712 mController.goToFilmstrip();
2713 mDataIdOnUserScrolling = currId;
2721 public void onLongPress(float x, float y) {
2722 final int dataId = getCurrentId();
2726 mListener.onFocusedDataLongPressed(dataId);
2730 public boolean onScroll(float x, float y, float dx, float dy) {
2731 final ViewItem currItem = mViewItem[mCurrentItem];
2732 if (currItem == null) {
2735 if (inFullScreen() && !mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
2739 // When image is zoomed in to be bigger than the screen
2741 ViewItem curr = mViewItem[mCurrentItem];
2742 float transX = curr.getTranslationX() * mScale - dx;
2743 float transY = curr.getTranslationY() * mScale - dy;
2744 curr.updateTransform(transX, transY, mScale, mScale, mDrawArea.width(),
2745 mDrawArea.height());
2748 int deltaX = (int) (dx / mScale);
2749 // Forces the current scrolling to stop.
2750 mController.stopScrolling(true);
2751 if (!mIsUserScrolling) {
2752 mIsUserScrolling = true;
2753 mDataIdOnUserScrolling = mViewItem[mCurrentItem].getId();
2755 if (inFilmstrip()) {
2756 // Disambiguate horizontal/vertical first.
2757 if (mScrollingDirection == SCROLL_DIR_NONE) {
2758 mScrollingDirection = (Math.abs(dx) > Math.abs(dy)) ? SCROLL_DIR_HORIZONTAL :
2759 SCROLL_DIR_VERTICAL;
2761 if (mScrollingDirection == SCROLL_DIR_HORIZONTAL) {
2762 if (mCenterX == currItem.getCenterX() && currItem.getId() == 0 && dx < 0) {
2763 // Already at the beginning, don't process the swipe.
2764 mIsUserScrolling = false;
2765 mScrollingDirection = SCROLL_DIR_NONE;
2768 mController.scroll(deltaX);
2770 // Vertical part. Promote or demote.
2772 Rect hitRect = new Rect();
2773 for (; hit < BUFFER_SIZE; hit++) {
2774 if (mViewItem[hit] == null) {
2777 mViewItem[hit].getHitRect(hitRect);
2778 if (hitRect.contains((int) x, (int) y)) {
2782 if (hit == BUFFER_SIZE) {
2787 ImageData data = mDataAdapter.getImageData(mViewItem[hit].getId());
2788 float transY = mViewItem[hit].getTranslationY() - dy / mScale;
2789 if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) &&
2793 if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) &&
2797 mViewItem[hit].setTranslationY(transY);
2799 } else if (inFullScreen()) {
2800 if (mViewItem[mCurrentItem] == null || (deltaX < 0 && mCenterX <=
2801 currItem.getCenterX() && currItem.getId() == 0)) {
2804 // Multiplied by 1.2 to make it more easy to swipe.
2805 mController.scroll((int) (deltaX * 1.2));
2813 public boolean onFling(float velocityX, float velocityY) {
2814 final ViewItem currItem = mViewItem[mCurrentItem];
2815 if (currItem == null) {
2818 if (!mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
2822 // Fling within the zoomed image
2823 mController.flingInsideZoomView(velocityX, velocityY);
2826 if (Math.abs(velocityX) < Math.abs(velocityY)) {
2827 // ignore vertical fling.
2831 // In full-screen, fling of a velocity above a threshold should go
2832 // to the next/prev photos
2833 if (mScale == FULL_SCREEN_SCALE) {
2834 int currItemCenterX = currItem.getCenterX();
2836 if (velocityX > 0) { // left
2837 if (mCenterX > currItemCenterX) {
2838 // The visually previous item is actually the current
2840 mController.scrollToPosition(
2841 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
2844 ViewItem prevItem = mViewItem[mCurrentItem - 1];
2845 if (prevItem == null) {
2848 mController.scrollToPosition(
2849 prevItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true);
2851 if (mController.stopScrolling(false)) {
2852 if (mCenterX < currItemCenterX) {
2853 // The visually next item is actually the current
2855 mController.scrollToPosition(
2856 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
2859 final ViewItem nextItem = mViewItem[mCurrentItem + 1];
2860 if (nextItem == null) {
2863 mController.scrollToPosition(
2864 nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true);
2865 if (isViewTypeSticky(currItem)) {
2866 mController.goToFilmstrip();
2872 if (mScale == FILM_STRIP_SCALE) {
2873 mController.fling(velocityX);
2879 public boolean onScaleBegin(float focusX, float focusY) {
2880 if (inCameraFullscreen()) {
2886 // If the image is smaller than screen size, we should allow to zoom
2887 // in to full screen size
2888 mMaxScale = Math.max(mController.getCurrentDataMaxScale(true), FULL_SCREEN_SCALE);
2893 public boolean onScale(float focusX, float focusY, float scale) {
2894 if (inCameraFullscreen()) {
2898 mScaleTrend = mScaleTrend * 0.3f + scale * 0.7f;
2899 float newScale = mScale * scale;
2900 if (mScale < FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) {
2901 if (newScale <= FILM_STRIP_SCALE) {
2902 newScale = FILM_STRIP_SCALE;
2904 // Scaled view is smaller than or equal to screen size both
2905 // before and after scaling
2906 if (mScale != newScale) {
2907 if (mScale == FILM_STRIP_SCALE) {
2910 if (newScale == FILM_STRIP_SCALE) {
2916 } else if (mScale < FULL_SCREEN_SCALE && newScale >= FULL_SCREEN_SCALE) {
2917 // Going from smaller than screen size to bigger than or equal
2919 if (mScale == FILM_STRIP_SCALE) {
2922 mScale = FULL_SCREEN_SCALE;
2923 onEnterFullScreen();
2924 mController.setSurroundingViewsVisible(false);
2926 } else if (mScale >= FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) {
2927 // Going from bigger than or equal to screen size to smaller
2929 if (inFullScreen()) {
2930 if (mFullScreenUIHidden) {
2931 onLeaveFullScreenUiHidden();
2933 onLeaveFullScreen();
2942 // Scaled view bigger than or equal to screen size both before
2943 // and after scaling
2944 if (!inZoomView()) {
2945 mController.setSurroundingViewsVisible(false);
2947 ViewItem curr = mViewItem[mCurrentItem];
2948 // Make sure the image is not overly scaled
2949 newScale = Math.min(newScale, mMaxScale);
2950 if (newScale == mScale) {
2953 float postScale = newScale / mScale;
2954 curr.postScale(focusX, focusY, postScale, mDrawArea.width(), mDrawArea.height());
2956 if (mScale == FULL_SCREEN_SCALE) {
2957 onEnterFullScreen();
2961 checkItemAtMaxSize();
2967 public void onScaleEnd() {
2968 zoomAtIndexChanged();
2969 if (mScale > FULL_SCREEN_SCALE + TOLERANCE) {
2972 mController.setSurroundingViewsVisible(true);
2973 if (mScale <= FILM_STRIP_SCALE + TOLERANCE) {
2974 mController.goToFilmstrip();
2975 } else if (mScaleTrend > 1f || mScale > FULL_SCREEN_SCALE - TOLERANCE) {
2977 mScale = FULL_SCREEN_SCALE;
2980 mController.goToFullScreen();
2982 mController.goToFilmstrip();