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.KeyEvent;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.accessibility.AccessibilityNodeInfo;
43 import android.view.animation.DecelerateInterpolator;
44 import android.widget.Scroller;
46 import com.android.camera.CameraActivity;
47 import com.android.camera.data.LocalData.ActionCallback;
48 import com.android.camera.debug.Log;
49 import com.android.camera.filmstrip.DataAdapter;
50 import com.android.camera.filmstrip.FilmstripController;
51 import com.android.camera.filmstrip.ImageData;
52 import com.android.camera.ui.FilmstripGestureRecognizer;
53 import com.android.camera.ui.ZoomView;
54 import com.android.camera.util.ApiHelper;
55 import com.android.camera.util.CameraUtil;
56 import com.android.camera2.R;
58 import java.lang.ref.WeakReference;
59 import java.util.ArrayDeque;
60 import java.util.Arrays;
61 import java.util.Queue;
63 public class FilmstripView extends ViewGroup {
65 * An action callback to be used for actions on the local media data items.
67 public static class ActionCallbackImpl implements ActionCallback {
68 private final WeakReference<Activity> mActivity;
71 * The given activity is used to start intents. It is wrapped in a weak
72 * reference to prevent leaks.
74 public ActionCallbackImpl(Activity activity) {
75 mActivity = new WeakReference<Activity>(activity);
79 * Fires an intent to play the video with the given URI and title.
82 public void playVideo(Uri uri, String title) {
83 Activity activity = mActivity.get();
84 if (activity != null) {
85 CameraUtil.playVideo(activity, uri, title);
91 private static final Log.Tag TAG = new Log.Tag("FilmstripView");
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;
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;
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;
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();
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];
135 private FilmstripController.FilmstripListener mListener;
136 private ZoomView mZoomView = null;
138 private MotionEvent mDown;
139 private boolean mCheckToIntercept = true;
141 private TimeInterpolator mViewAnimInterpolator;
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;
148 private boolean mFullScreenUIHidden = false;
149 private final SparseArray<Queue<View>> recycledViews = new SparseArray<Queue<View>>();
152 * A helper class to tract and calculate the view coordination.
154 private class ViewItem {
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;
163 private ValueAnimator mTranslationXAnimator;
164 private ValueAnimator mTranslationYAnimator;
165 private ValueAnimator mAlphaAnimator;
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.
174 public ViewItem(int id, View v, ImageData data) {
180 mMaximumBitmapRequested = false;
182 mViewArea = new RectF();
185 public boolean isMaximumBitmapRequested() {
186 return mMaximumBitmapRequested;
189 public void setMaximumBitmapRequested() {
190 mMaximumBitmapRequested = true;
194 * Returns the data id from
195 * {@link com.android.camera.filmstrip.DataAdapter}.
202 * Sets the data id from
203 * {@link com.android.camera.filmstrip.DataAdapter}.
205 public void setId(int id) {
209 /** Sets the left position of the view in the whole filmstrip. */
210 public void setLeftPosition(int pos) {
214 /** Returns the left position of the view in the whole filmstrip. */
215 public int getLeftPosition() {
216 return mLeftPosition;
219 /** Returns the translation of Y regarding the view scale. */
220 public float getTranslationY() {
221 return mView.getTranslationY() / mScale;
224 /** Returns the translation of X regarding the view scale. */
225 public float getTranslationX() {
226 return mView.getTranslationX() / mScale;
229 /** Sets the translation of Y regarding the view scale. */
230 public void setTranslationY(float transY) {
231 mView.setTranslationY(transY * mScale);
234 /** Sets the translation of X regarding the view scale. */
235 public void setTranslationX(float transX) {
236 mView.setTranslationX(transX * mScale);
239 /** Forwarding of {@link android.view.View#setAlpha(float)}. */
240 public void setAlpha(float alpha) {
241 mView.setAlpha(alpha);
244 /** Forwarding of {@link android.view.View#getAlpha()}. */
245 public float getAlpha() {
246 return mView.getAlpha();
249 /** Forwarding of {@link android.view.View#getMeasuredWidth()}. */
250 public int getMeasuredWidth() {
251 return mView.getMeasuredWidth();
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
261 * @param targetX The final value.
262 * @param duration_ms The duration of the animation.
263 * @param interpolator Time interpolator.
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() {
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().
280 runAnimation(mTranslationXAnimator, getTranslationX(), targetX, duration_ms,
285 * Animates the Y translation of the view.
287 * @param targetY The final value.
288 * @param duration_ms The duration of the animation.
289 * @param interpolator Time interpolator.
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() {
297 public void onAnimationUpdate(ValueAnimator valueAnimator) {
298 setTranslationY((Float) valueAnimator.getAnimatedValue());
302 runAnimation(mTranslationYAnimator, getTranslationY(), targetY, duration_ms,
307 * Animates the alpha value of the view.
309 * @param targetAlpha The final value.
310 * @param duration_ms The duration of the animation.
311 * @param interpolator Time interpolator.
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() {
319 public void onAnimationUpdate(ValueAnimator valueAnimator) {
320 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue());
324 runAnimation(mAlphaAnimator, getAlpha(), targetAlpha, duration_ms, interpolator);
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) {
333 animator.setInterpolator(interpolator);
334 animator.setDuration(duration_ms);
335 animator.setFloatValues(startValue, targetValue);
339 /** Adjusts the translation of X regarding the view scale. */
340 public void translateXScaledBy(float transX) {
341 setTranslationX(getTranslationX() + transX * mScale);
345 * Forwarding of {@link android.view.View#getHitRect(android.graphics.Rect)}.
347 public void getHitRect(Rect rect) {
348 mView.getHitRect(rect);
351 public int getCenterX() {
352 return mLeftPosition + mView.getMeasuredWidth() / 2;
355 /** Forwarding of {@link android.view.View#getVisibility()}. */
356 public int getVisibility() {
357 return mView.getVisibility();
360 /** Forwarding of {@link android.view.View#setVisibility(int)}. */
361 public void setVisibility(int visibility) {
362 mView.setVisibility(visibility);
366 * Notifies the {@link com.android.camera.filmstrip.DataAdapter} to
369 public void resizeView(Context context, int w, int h) {
370 mDataAdapter.resizeView(context, mDataId, mView, w, h);
374 * Adds the view of the data to the view hierarchy if necessary.
376 public void addViewToHierarchy() {
377 if (indexOfChild(mView) < 0) {
382 setVisibility(View.VISIBLE);
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.
392 * @param force {@code true} to remove the view from the hierarchy
393 * regardless of the view type.
395 public void removeViewFromHierarchy(boolean force) {
396 if (force || mData.getViewType() != ImageData.VIEW_TYPE_STICKY) {
398 mData.recycle(mView);
399 recycleView(mView, mDataId);
401 setVisibility(View.INVISIBLE);
406 * Brings the view to front by
407 * {@link #bringChildToFront(android.view.View)}
409 public void bringViewToFront() {
410 bringChildToFront(mView);
414 * The visual x position of this view, in pixels.
416 public float getX() {
421 * The visual y position of this view, in pixels.
423 public float getY() {
428 * Forwarding of {@link android.view.View#measure(int, int)}.
430 public void measure(int widthSpec, int heightSpec) {
431 mView.measure(widthSpec, heightSpec);
434 private void layoutAt(int left, int top) {
435 mView.layout(left, top, left + mView.getMeasuredWidth(),
436 top + mView.getMeasuredHeight());
440 * The bounding rect of the view.
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();
451 private View getView() {
456 * Layouts the view in the area assuming the center of the area is at a
457 * specific point of the whole filmstrip.
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.
464 public void layoutWithTranslationX(Rect drawArea, int refCenter, float scale) {
465 final float translationX =
466 ((mTranslationXAnimator != null && mTranslationXAnimator.isRunning()) ?
467 (Float) mTranslationXAnimator.getAnimatedValue() : 0);
469 (int) (drawArea.centerX() + (mLeftPosition - refCenter + translationX) * scale);
470 int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale);
472 mView.setScaleX(scale);
473 mView.setScaleY(scale);
475 // update mViewArea for touch detection.
476 int l = mView.getLeft();
477 int t = mView.getTop();
479 l + mView.getMeasuredWidth() * scale,
480 t + mView.getMeasuredHeight() * scale);
483 /** Returns true if the point is in the view. */
484 public boolean areaContains(float x, float y) {
485 return mViewArea.contains(x, y);
489 * Return the width of the view.
491 public int getWidth() {
492 return mView.getWidth();
496 * Returns the position of the left edge of the view area content is drawn in.
498 public int getDrawAreaLeft() {
499 return Math.round(mViewArea.left);
502 public void copyAttributes(ViewItem item) {
503 setLeftPosition(item.getLeftPosition());
505 setTranslationX(item.getTranslationX());
506 if (item.mTranslationXAnimator != null) {
507 mTranslationXAnimator = item.mTranslationXAnimator;
508 mTranslationXAnimator.removeAllUpdateListeners();
509 mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
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().
521 setTranslationY(item.getTranslationY());
522 if (item.mTranslationYAnimator != null) {
523 mTranslationYAnimator = item.mTranslationYAnimator;
524 mTranslationYAnimator.removeAllUpdateListeners();
525 mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
527 public void onAnimationUpdate(ValueAnimator valueAnimator) {
528 setTranslationY((Float) valueAnimator.getAnimatedValue());
533 setAlpha(item.getAlpha());
534 if (item.mAlphaAnimator != null) {
535 mAlphaAnimator = item.mAlphaAnimator;
536 mAlphaAnimator.removeAllUpdateListeners();
537 mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
539 public void onAnimationUpdate(ValueAnimator valueAnimator) {
540 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue());
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.
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,
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);
581 void resetTransform() {
582 mView.setScaleX(FULL_SCREEN_SCALE);
583 mView.setScaleY(FULL_SCREEN_SCALE);
584 mView.setTranslationX(0f);
585 mView.setTranslationY(0f);
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();
601 public FilmstripView(Context context) {
603 init((CameraActivity) context);
607 public FilmstripView(Context context, AttributeSet attrs) {
608 super(context, attrs);
609 init((CameraActivity) context);
613 public FilmstripView(Context context, AttributeSet attrs, int defStyle) {
614 super(context, attrs, defStyle);
615 init((CameraActivity) context);
618 private void init(CameraActivity cameraActivity) {
619 setWillNotDraw(false);
620 mActivity = cameraActivity;
621 mActionCallback = new ActionCallbackImpl(mActivity);
623 mDataIdOnUserScrolling = 0;
624 mController = new MyController(cameraActivity);
625 mViewAnimInterpolator = new DecelerateInterpolator();
626 mZoomView = new ZoomView(cameraActivity);
627 mZoomView.setVisibility(GONE);
630 mGestureListener = new MyGestureReceiver();
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;
645 setAccessibilityDelegate(new AccessibilityDelegate() {
647 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
648 super.onInitializeAccessibilityNodeInfo(host, info);
650 info.setClassName(FilmstripView.class.getName());
651 info.setScrollable(true);
652 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
653 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
657 public boolean performAccessibilityAction(View host, int action, Bundle args) {
658 if (!mController.isScrolling()) {
660 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
661 mController.goToNextItem();
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();
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);
681 return super.performAccessibilityAction(host, action, args);
686 private void recycleView(View view, int dataId) {
687 final int viewType = (Integer) view.getTag(R.id.mediadata_tag_viewtype);
689 Queue<View> recycledViewsForType = recycledViews.get(viewType);
690 if (recycledViewsForType == null) {
691 recycledViewsForType = new ArrayDeque<View>();
692 recycledViews.put(viewType, recycledViewsForType);
694 recycledViewsForType.offer(view);
698 private View getRecycledView(int dataId) {
699 final int viewType = mDataAdapter.getItemViewType(dataId);
700 Queue<View> recycledViewsForType = recycledViews.get(viewType);
702 if (recycledViewsForType != null) {
703 result = recycledViewsForType.poll();
709 * Returns the controller.
711 * @return The {@code Controller}.
713 public FilmstripController getController() {
718 * Returns the draw area width of the current item.
720 public int getCurrentItemLeft() {
721 return mViewItem[mCurrentItem].getDrawAreaLeft();
724 private void setListener(FilmstripController.FilmstripListener l) {
728 private void setViewGap(int viewGap) {
729 mViewGapInPixel = viewGap;
733 * Called after current item or zoom level has changed.
735 public void zoomAtIndexChanged() {
736 if (mViewItem[mCurrentItem] == null) {
739 int id = mViewItem[mCurrentItem].getId();
740 mListener.onZoomAtIndexChanged(id, mScale);
744 * Checks if the data is at the center.
746 * @param id The id of the data to check.
747 * @return {@code True} if the data is currently at the center.
749 private boolean isDataAtCenter(int id) {
750 if (mViewItem[mCurrentItem] == null) {
753 if (mViewItem[mCurrentItem].getId() == id
754 && isCurrentItemCentered()) {
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");
768 Point dim = CameraUtil.resizeToFill(imageData.getWidth(), imageData.getHeight(),
769 imageData.getRotation(), boundWidth, boundHeight);
771 item.measure(MeasureSpec.makeMeasureSpec(dim.x, MeasureSpec.EXACTLY),
772 MeasureSpec.makeMeasureSpec(dim.y, MeasureSpec.EXACTLY));
776 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
777 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
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.
786 for (ViewItem item : mViewItem) {
788 measureViewItem(item, boundWidth, boundHeight);
793 mZoomView.measure(MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.EXACTLY),
794 MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.EXACTLY));
797 private int findTheNearestView(int pointX) {
800 // Find the first non-null ViewItem.
801 while (nearest < BUFFER_SIZE
802 && (mViewItem[nearest] == null || mViewItem[nearest].getLeftPosition() == -1)) {
805 // No existing available ViewItem
806 if (nearest == BUFFER_SIZE) {
810 int min = Math.abs(pointX - mViewItem[nearest].getCenterX());
812 for (int itemID = nearest + 1; itemID < BUFFER_SIZE && mViewItem[itemID] != null; itemID++) {
814 if (mViewItem[itemID].getLeftPosition() == -1) {
818 int c = mViewItem[itemID].getCenterX();
819 int dist = Math.abs(pointX - c);
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");
835 ImageData data = mDataAdapter.getImageData(dataID);
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);
849 View recycled = getRecycledView(dataID);
850 View v = mDataAdapter.getView(mActivity.getAndroidContext(), recycled, dataID,
855 ViewItem item = new ViewItem(dataID, v, data);
856 item.addViewToHierarchy();
860 private void checkItemAtMaxSize() {
861 ViewItem item = mViewItem[mCurrentItem];
862 if (item.isMaximumBitmapRequested()) {
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);
873 private void removeItem(int itemID) {
874 if (itemID >= mViewItem.length || mViewItem[itemID] == null) {
877 ImageData data = mDataAdapter.getImageData(mViewItem[itemID].getId());
879 Log.e(TAG, "trying to remove a null item");
882 mViewItem[itemID].removeViewFromHierarchy(false);
883 mViewItem[itemID] = null;
887 * We try to keep the one closest to the center of the screen at position
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.
896 final int nearest = findTheNearestView(mCenterX);
898 if (nearest == -1 || nearest == mCurrentItem) {
901 int prevDataId = (mViewItem[mCurrentItem] == null ? -1 : mViewItem[mCurrentItem].getId());
902 final int adjust = nearest - mCurrentItem;
904 for (int k = 0; k < adjust; k++) {
907 for (int k = 0; k + adjust < BUFFER_SIZE; k++) {
908 mViewItem[k] = mViewItem[k + adjust];
910 for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) {
912 if (mViewItem[k - 1] != null) {
913 mViewItem[k] = buildItemFromData(mViewItem[k - 1].getId() + 1);
918 for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) {
921 for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) {
922 mViewItem[k] = mViewItem[k + adjust];
924 for (int k = -1 - adjust; k >= 0; k--) {
926 if (mViewItem[k + 1] != null) {
927 mViewItem[k] = buildItemFromData(mViewItem[k + 1].getId() - 1);
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);
939 zoomAtIndexChanged();
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.
946 * @return Whether clamp happened.
948 private boolean clampCenterX() {
949 ViewItem curr = mViewItem[mCurrentItem];
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()) {
960 if (curr.getId() == 0 && mCenterX < curr.getCenterX()) {
961 // Stop at the first ViewItem.
965 if (curr.getId() == mDataAdapter.getTotalNumber() - 1
966 && mCenterX > curr.getCenterX()) {
972 mCenterX = curr.getCenterX();
979 * Reorders the child views to be consistent with their data ID. This method
980 * should be called after adding/removing views.
982 private void adjustChildZOrder() {
983 for (int i = BUFFER_SIZE - 1; i >= 0; i--) {
984 if (mViewItem[i] == null) {
987 mViewItem[i].bringViewToFront();
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);
997 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
998 private void setMaxElevation(View v) {
999 v.setElevation(Float.MAX_VALUE);
1003 * Returns the ID of the current item, or -1 if there is no data.
1005 private int getCurrentId() {
1006 ViewItem current = mViewItem[mCurrentItem];
1007 if (current == null) {
1010 return current.getId();
1014 * Keep the current item in the center. This functions does not check if the
1015 * current item is null.
1017 private void snapInCenter() {
1018 final ViewItem currItem = mViewItem[mCurrentItem];
1019 if (currItem == null) {
1022 final int currentViewCenter = currItem.getCenterX();
1023 if (mController.isScrolling() || mIsUserScrolling
1024 || isCurrentItemCentered()) {
1028 int snapInTime = (int) (SNAP_IN_CENTER_TIME_MS
1029 * ((float) Math.abs(mCenterX - currentViewCenter))
1030 / mDrawArea.width());
1031 mController.scrollToPosition(currentViewCenter,
1033 if (isViewTypeSticky(currItem) && !mController.isScaling() && mScale != FULL_SCREEN_SCALE) {
1034 // Now going to full screen camera
1035 mController.goToFullScreen();
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.
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}.
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.");
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 = "
1066 final int currCenterX = curr.getCenterX();
1067 final int nextCenterX = next.getCenterX();
1068 final int translate = (int) ((nextCenterX - drawAreaWidth
1069 - currCenterX) * scaleFraction);
1071 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1073 curr.setVisibility(VISIBLE);
1075 if (inFullScreen()) {
1076 curr.setTranslationX(translate * (mCenterX - currCenterX) / (nextCenterX - currCenterX));
1078 curr.setTranslationX(translate);
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.
1086 * @param currItemId The ID of the item to fade.
1088 private void fadeAndScaleRightViewItem(int currItemId) {
1089 if (currItemId < 1 || currItemId > BUFFER_SIZE) {
1090 Log.e(TAG, "currItem id out of bound.");
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 = "
1102 if (currItemId > mCurrentItem + 1) {
1103 // Every item not right next to the mCurrentItem is invisible.
1104 currItem.setVisibility(INVISIBLE);
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);
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);
1124 private void layoutViewItems(boolean layoutChanged) {
1125 if (mViewItem[mCurrentItem] == null ||
1126 mDrawArea.width() == 0 ||
1127 mDrawArea.height() == 0) {
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);
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}
1146 final float scaleFraction = mViewAnimInterpolator.getInterpolation(
1147 (mScale - FILM_STRIP_SCALE) / (FULL_SCREEN_SCALE - FILM_STRIP_SCALE));
1148 final int fullScreenWidth = mDrawArea.width() + mViewGapInPixel;
1150 // Decide the position for all view items on the left and the right
1154 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
1155 final ViewItem curr = mViewItem[itemID];
1160 // First, layout relatively to the next one.
1161 final int currLeft = mViewItem[itemID + 1].getLeftPosition()
1162 - curr.getMeasuredWidth() - mViewGapInPixel;
1163 curr.setLeftPosition(currLeft);
1166 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
1167 final ViewItem curr = mViewItem[itemID];
1172 // First, layout relatively to the previous one.
1173 final ViewItem prev = mViewItem[itemID - 1];
1174 final int currLeft =
1175 prev.getLeftPosition() + prev.getMeasuredWidth()
1177 curr.setLeftPosition(currLeft);
1180 // Special case for the one immediately on the right of the camera
1182 boolean immediateRight =
1183 (mViewItem[mCurrentItem].getId() == 1 &&
1184 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY);
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);
1208 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1209 currItem.setTranslationX(0f);
1210 currItem.setAlpha(1f);
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);
1221 final int currCenterX = currItem.getCenterX();
1222 final int prevCenterX = mViewItem[mCurrentItem - 1].getCenterX();
1223 final float fadeDownFraction =
1224 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX);
1226 (1 - fadeDownFraction) * (1 - scaleFraction) + fadeDownFraction);
1230 // Layout the rest dependent on the current scale.
1232 // Items on the left
1233 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
1234 final ViewItem curr = mViewItem[itemID];
1238 translateLeftViewItem(itemID, fullScreenWidth, scaleFraction);
1241 // Items on the right
1242 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
1243 final ViewItem curr = mViewItem[itemID];
1248 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1249 if (curr.getId() == 1 && isViewTypeSticky(curr)) {
1250 // Special case for the one next to the camera preview.
1255 if (scaleFraction == 1) {
1256 // It's in full-screen mode.
1257 fadeAndScaleRightViewItem(itemID);
1259 boolean setToVisible = (curr.getVisibility() == INVISIBLE);
1261 if (itemID == mCurrentItem + 1) {
1262 curr.setAlpha(1f - scaleFraction);
1264 if (scaleFraction == 0f) {
1267 setToVisible = false;
1272 curr.setVisibility(VISIBLE);
1275 curr.setTranslationX(
1276 (mViewItem[mCurrentItem].getLeftPosition() - curr.getLeftPosition()) *
1284 private boolean isViewTypeSticky(ViewItem item) {
1288 return mDataAdapter.getImageData(item.getId()).getViewType() ==
1289 ImageData.VIEW_TYPE_STICKY;
1293 public void onDraw(Canvas c) {
1294 // TODO: remove layoutViewItems() here.
1295 layoutViewItems(false);
1300 protected void onLayout(boolean changed, int l, int t, int r, int b) {
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
1309 if (!inZoomView() || changed) {
1311 layoutViewItems(changed);
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).
1321 private void resetZoomView() {
1322 if (!inZoomView()) {
1325 ViewItem current = mViewItem[mCurrentItem];
1326 if (current == null) {
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);
1338 private void hideZoomView() {
1340 mController.cancelLoadingZoomedImage();
1341 mZoomView.setVisibility(GONE);
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);
1351 private void animateItemRemoval(int dataID, final ImageData data) {
1352 if (mScale > FULL_SCREEN_SCALE) {
1355 int removedItemId = findItemByDataID(dataID);
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) {
1362 mViewItem[i].setId(mViewItem[i].getId() - 1);
1364 if (removedItemId == -1) {
1368 final ViewItem removedItem = mViewItem[removedItemId];
1369 final int offsetX = removedItem.getMeasuredWidth() + mViewGapInPixel;
1371 for (int i = removedItemId + 1; i < BUFFER_SIZE; i++) {
1372 if (mViewItem[i] != null) {
1373 mViewItem[i].setLeftPosition(mViewItem[i].getLeftPosition() - offsetX);
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
1382 for (int i = removedItemId; i < BUFFER_SIZE - 1; i++) {
1383 mViewItem[i] = mViewItem[i + 1];
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);
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);
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);
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);
1424 // CurrItem should NOT be NULL, but if is, at least don't crash.
1425 Log.w(TAG,"Caught invalid update in removal animation.");
1428 // fill the removed place by right shift
1429 mCenterX -= offsetX;
1431 for (int i = removedItemId; i > 0; i--) {
1432 mViewItem[i] = mViewItem[i - 1];
1435 // pull data out from the DataAdapter for the first one.
1437 int next = curr + 1;
1438 if (mViewItem[next] != null) {
1439 mViewItem[curr] = buildItemFromData(mViewItem[next].getId() - 1);
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);
1450 int transY = getHeight() / 8;
1451 if (removedItem.getTranslationY() < 0) {
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() {
1460 removedItem.removeViewFromHierarchy(false);
1462 }, GEOMETRY_ADJUST_TIME_MS);
1464 adjustChildZOrder();
1467 // Now, slide every one back.
1468 if (mViewItem[mCurrentItem] == null) {
1471 for (int i = 0; i < BUFFER_SIZE; i++) {
1472 if (mViewItem[i] != null
1473 && mViewItem[i].getTranslationX() != 0f) {
1474 slideViewBack(mViewItem[i]);
1477 if (isCurrentItemCentered() && isViewTypeSticky(mViewItem[mCurrentItem])) {
1478 // Special case for scrolling onto the camera preview after removal.
1479 mController.goToFullScreen();
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) {
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
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;
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) {
1514 mViewItem[i].setId(mViewItem[i].getId() + 1);
1516 if (insertedItemId == -1) {
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");
1531 if (insertedItemId >= mCurrentItem) {
1532 if (insertedItemId == mCurrentItem) {
1533 viewItem.setLeftPosition(mViewItem[mCurrentItem].getLeftPosition());
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]);
1545 // Shift left. Put the inserted data on the left instead of the
1548 if (insertedItemId < 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];
1561 mViewItem[insertedItemId] = viewItem;
1562 viewItem.setAlpha(0f);
1563 viewItem.setTranslationY(getHeight() / 8);
1564 slideViewBack(viewItem);
1565 adjustChildZOrder();
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() {
1576 public void onDataLoaded() {
1581 public void onDataUpdated(DataAdapter.UpdateReporter reporter) {
1586 public void onDataInserted(int dataId, ImageData data) {
1587 if (mViewItem[mCurrentItem] == null) {
1588 // empty now, simply do a reload.
1591 updateInsertion(dataId);
1593 if (mListener != null) {
1594 mListener.onDataFocusChanged(dataId, getCurrentId());
1599 public void onDataRemoved(int dataId, ImageData data) {
1600 animateItemRemoval(dataId, data);
1601 if (mListener != null) {
1602 mListener.onDataFocusChanged(dataId, getCurrentId());
1608 private boolean inFilmstrip() {
1609 return (mScale == FILM_STRIP_SCALE);
1612 private boolean inFullScreen() {
1613 return (mScale == FULL_SCREEN_SCALE);
1616 private boolean inZoomView() {
1617 return (mScale > FULL_SCREEN_SCALE);
1620 private boolean isCameraPreview() {
1621 return isViewTypeSticky(mViewItem[mCurrentItem]);
1624 private boolean inCameraFullscreen() {
1625 return isDataAtCenter(0) && inFullScreen()
1626 && (isViewTypeSticky(mViewItem[mCurrentItem]));
1630 public boolean onInterceptTouchEvent(MotionEvent ev) {
1631 if (mController.isScrolling()) {
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;
1644 } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
1645 // Do not intercept touch once child is in zoom mode
1646 mCheckToIntercept = false;
1649 if (!mCheckToIntercept) {
1652 if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) {
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) {
1669 public boolean onTouchEvent(MotionEvent ev) {
1670 return mGestureRecognizer.onTouchEvent(ev);
1674 public boolean onGenericMotionEvent(MotionEvent ev) {
1675 mGestureRecognizer.onGenericMotionEvent(ev);
1679 FilmstripGestureRecognizer.Listener getGestureListener() {
1680 return mGestureListener;
1683 private void updateViewItem(int itemID) {
1684 ViewItem item = mViewItem[itemID];
1686 Log.e(TAG, "trying to update an null item");
1689 item.removeViewFromHierarchy(true);
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();
1698 newItem.copyAttributes(item);
1699 mViewItem[itemID] = newItem;
1700 mZoomView.resetDecoder();
1702 boolean stopScroll = clampCenterX();
1704 mController.stopScrolling(true);
1706 adjustChildZOrder();
1708 if (mListener != null) {
1709 mListener.onDataUpdated(newItem.getId());
1713 /** Some of the data is changed. */
1714 private void update(DataAdapter.UpdateReporter reporter) {
1716 if (mViewItem[mCurrentItem] == null) {
1721 // Check the current one.
1722 ViewItem curr = mViewItem[mCurrentItem];
1723 int dataId = curr.getId();
1724 if (reporter.isDataRemoved(dataId)) {
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;
1741 for (int i = mCurrentItem - 1; i >= 0; i--) {
1742 curr = mViewItem[i];
1744 dataId = curr.getId();
1745 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
1749 ViewItem next = mViewItem[i + 1];
1751 mViewItem[i] = buildItemFromData(next.getId() - 1);
1757 for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) {
1758 curr = mViewItem[i];
1760 dataId = curr.getId();
1761 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
1765 ViewItem prev = mViewItem[i - 1];
1767 mViewItem[i] = buildItemFromData(prev.getId() + 1);
1771 adjustChildZOrder();
1772 // Request a layout to find the measured width/height of the view first.
1774 // Update photo sphere visibility after metadata fully written.
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
1782 private void reload() {
1783 mController.stopScrolling(true);
1784 mController.stopScale();
1785 mDataIdOnUserScrolling = 0;
1788 if (mViewItem[mCurrentItem] != null) {
1789 prevId = mViewItem[mCurrentItem].getId();
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) {
1797 mViewItem[i].removeViewFromHierarchy(false);
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) {
1807 mViewItem[mCurrentItem] = buildItemFromData(0);
1808 if (mViewItem[mCurrentItem] == null) {
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) {
1819 // Ensure that the views in mViewItem will layout the first in the
1820 // center of the display upon a reload.
1822 mScale = FILM_STRIP_SCALE;
1824 adjustChildZOrder();
1827 if (mListener != null) {
1828 mListener.onDataReloaded();
1829 mListener.onDataFocusChanged(prevId, mViewItem[mCurrentItem].getId());
1833 private void promoteData(int itemID, int dataID) {
1834 if (mListener != null) {
1835 mListener.onFocusedDataPromoted(dataID);
1839 private void demoteData(int itemID, int dataID) {
1840 if (mListener != null) {
1841 mListener.onFocusedDataDemoted(dataID);
1845 private void onEnterFilmstrip() {
1846 if (mListener != null) {
1847 mListener.onEnterFilmstrip(getCurrentId());
1851 private void onLeaveFilmstrip() {
1852 if (mListener != null) {
1853 mListener.onLeaveFilmstrip(getCurrentId());
1857 private void onEnterFullScreen() {
1858 mFullScreenUIHidden = false;
1859 if (mListener != null) {
1860 mListener.onEnterFullScreenUiShown(getCurrentId());
1864 private void onLeaveFullScreen() {
1865 if (mListener != null) {
1866 mListener.onLeaveFullScreenUiShown(getCurrentId());
1870 private void onEnterFullScreenUiHidden() {
1871 mFullScreenUIHidden = true;
1872 if (mListener != null) {
1873 mListener.onEnterFullScreenUiHidden(getCurrentId());
1877 private void onLeaveFullScreenUiHidden() {
1878 mFullScreenUIHidden = false;
1879 if (mListener != null) {
1880 mListener.onLeaveFullScreenUiHidden(getCurrentId());
1884 private void onEnterZoomView() {
1885 if (mListener != null) {
1886 mListener.onEnterZoomView(getCurrentId());
1890 private void onLeaveZoomView() {
1891 mController.setSurroundingViewsVisible(true);
1895 * MyController controls all the geometry animations. It passively tells the
1896 * geometry information on demand.
1898 private class MyController implements FilmstripController {
1900 private final ValueAnimator mScaleAnimator;
1901 private ValueAnimator mZoomAnimator;
1902 private AnimatorSet mFlingAnimator;
1904 private final MyScroller mScroller;
1905 private boolean mCanStopScroll;
1907 private final MyScroller.Listener mScrollerListener =
1908 new MyScroller.Listener() {
1910 public void onScrollUpdate(int currX, int currY) {
1913 boolean stopScroll = clampCenterX();
1915 mController.stopScrolling(true);
1921 public void onScrollEnd() {
1922 mCanStopScroll = true;
1923 if (mViewItem[mCurrentItem] == null) {
1927 if (isCurrentItemCentered()
1928 && isViewTypeSticky(mViewItem[mCurrentItem])) {
1929 // Special case for the scrolling end on the camera
1936 private final ValueAnimator.AnimatorUpdateListener mScaleAnimatorUpdateListener =
1937 new ValueAnimator.AnimatorUpdateListener() {
1939 public void onAnimationUpdate(ValueAnimator animation) {
1940 if (mViewItem[mCurrentItem] == null) {
1943 mScale = (Float) animation.getAnimatedValue();
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;
1955 mScaleAnimator = new ValueAnimator();
1956 mScaleAnimator.addUpdateListener(mScaleAnimatorUpdateListener);
1957 mScaleAnimator.setInterpolator(decelerateInterpolator);
1958 mScaleAnimator.addListener(new Animator.AnimatorListener() {
1960 public void onAnimationStart(Animator animator) {
1961 if (mScale == FULL_SCREEN_SCALE) {
1962 onLeaveFullScreen();
1964 if (mScale == FILM_STRIP_SCALE) {
1971 public void onAnimationEnd(Animator animator) {
1972 if (mScale == FULL_SCREEN_SCALE) {
1973 onEnterFullScreen();
1975 if (mScale == FILM_STRIP_SCALE) {
1979 zoomAtIndexChanged();
1983 public void onAnimationCancel(Animator animator) {
1988 public void onAnimationRepeat(Animator animator) {
1995 public void setImageGap(int imageGap) {
1996 FilmstripView.this.setViewGap(imageGap);
2000 public int getCurrentId() {
2001 return FilmstripView.this.getCurrentId();
2005 public void setDataAdapter(DataAdapter adapter) {
2006 FilmstripView.this.setDataAdapter(adapter);
2010 public boolean inFilmstrip() {
2011 return FilmstripView.this.inFilmstrip();
2015 public boolean inFullScreen() {
2016 return FilmstripView.this.inFullScreen();
2020 public boolean isCameraPreview() {
2021 return FilmstripView.this.isCameraPreview();
2025 public boolean inCameraFullscreen() {
2026 return FilmstripView.this.inCameraFullscreen();
2030 public void setListener(FilmstripListener l) {
2031 FilmstripView.this.setListener(l);
2035 public boolean isScrolling() {
2036 return !mScroller.isFinished();
2040 public boolean isScaling() {
2041 return mScaleAnimator.isRunning();
2044 private int estimateMinX(int dataID, int leftPos, int viewWidth) {
2045 return leftPos - (dataID + 100) * (viewWidth + mViewGapInPixel);
2048 private int estimateMaxX(int dataID, int leftPos, int viewWidth) {
2050 + (mDataAdapter.getTotalNumber() - dataID + 100)
2051 * (viewWidth + mViewGapInPixel);
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();
2060 // Calculate end scale
2061 final float maxScale = getCurrentDataMaxScale(false);
2062 final float endScale = mScale < maxScale - maxScale * TOLERANCE
2063 ? maxScale : FULL_SCREEN_SCALE;
2065 mZoomAnimator = new ValueAnimator();
2066 mZoomAnimator.setFloatValues(mScale, endScale);
2067 mZoomAnimator.setDuration(ZOOM_ANIMATION_DURATION_MS);
2068 mZoomAnimator.addListener(new Animator.AnimatorListener() {
2070 public void onAnimationStart(Animator animation) {
2071 if (mScale == FULL_SCREEN_SCALE) {
2072 if (mFullScreenUIHidden) {
2073 onLeaveFullScreenUiHidden();
2075 onLeaveFullScreen();
2077 setSurroundingViewsVisible(false);
2078 } else if (inZoomView()) {
2081 cancelLoadingZoomedImage();
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());
2094 if (inFullScreen()) {
2095 setSurroundingViewsVisible(true);
2096 mZoomView.setVisibility(GONE);
2097 current.resetTransform();
2098 onEnterFullScreenUiHidden();
2100 mController.loadZoomedImage();
2103 mZoomAnimator = null;
2104 zoomAtIndexChanged();
2108 public void onAnimationCancel(Animator animation) {
2113 public void onAnimationRepeat(Animator animation) {
2118 mZoomAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2120 public void onAnimationUpdate(ValueAnimator animation) {
2121 float newScale = (Float) animation.getAnimatedValue();
2122 float postScale = newScale / mScale;
2124 current.postScale(focusX, focusY, postScale, mDrawArea.width(),
2125 mDrawArea.height());
2128 mZoomAnimator.start();
2132 public void scroll(float deltaX) {
2133 if (!stopScrolling(false)) {
2138 boolean stopScroll = clampCenterX();
2140 mController.stopScrolling(true);
2146 public void fling(float velocityX) {
2147 if (!stopScrolling(false)) {
2150 final ViewItem item = mViewItem[mCurrentItem];
2155 float scaledVelocityX = velocityX / mScale;
2156 if (inFullScreen() && isViewTypeSticky(item) && scaledVelocityX < 0) {
2157 // Swipe left in camera preview.
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);
2172 void flingInsideZoomView(float velocityX, float velocityY) {
2173 if (!inZoomView()) {
2177 final ViewItem current = mViewItem[mCurrentItem];
2178 if (current == null) {
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
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))
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))));
2203 final float translationX = current.getTranslationX() * mScale;
2204 final float translationY = current.getTranslationY() * mScale;
2206 final ValueAnimator decelerationX = ValueAnimator.ofFloat(translationX,
2207 translationX + duration / factor * velocityX);
2208 final ValueAnimator decelerationY = ValueAnimator.ofFloat(translationY,
2209 translationY + duration / factor * velocityY);
2211 decelerationY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2213 public void onAnimationUpdate(ValueAnimator animation) {
2214 float transX = (Float) decelerationX.getAnimatedValue();
2215 float transY = (Float) decelerationY.getAnimatedValue();
2217 current.updateTransform(transX, transY, mScale,
2218 mScale, mDrawArea.width(), mDrawArea.height());
2222 mFlingAnimator = new AnimatorSet();
2223 mFlingAnimator.play(decelerationX).with(decelerationY);
2224 mFlingAnimator.setDuration((int) (duration * 1000));
2225 mFlingAnimator.setInterpolator(new TimeInterpolator() {
2227 public float getInterpolation(float input) {
2228 return (float) (1.0f - Math.pow((1.0f - input), factor));
2231 mFlingAnimator.addListener(new Animator.AnimatorListener() {
2232 private boolean mCancelled = false;
2235 public void onAnimationStart(Animator animation) {
2240 public void onAnimationEnd(Animator animation) {
2244 mFlingAnimator = null;
2248 public void onAnimationCancel(Animator animation) {
2253 public void onAnimationRepeat(Animator animation) {
2257 mFlingAnimator.start();
2261 public boolean stopScrolling(boolean forced) {
2262 if (!isScrolling()) {
2264 } else if (!mCanStopScroll && !forced) {
2267 mScroller.forceFinished(true);
2271 private void stopScale() {
2272 mScaleAnimator.cancel();
2276 public void scrollToPosition(int position, int duration, boolean interruptible) {
2277 if (mViewItem[mCurrentItem] == null) {
2280 mCanStopScroll = interruptible;
2281 mScroller.startScroll(mCenterX, 0, position - mCenterX, 0, duration);
2285 public boolean goToNextItem() {
2286 return goToItem(mCurrentItem + 1);
2290 public boolean goToPreviousItem() {
2291 return goToItem(mCurrentItem - 1);
2294 private boolean goToItem(int itemIndex) {
2295 final ViewItem nextItem = mViewItem[itemIndex];
2296 if (nextItem == null) {
2299 stopScrolling(true);
2300 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS * 2, false);
2302 if (isViewTypeSticky(mViewItem[mCurrentItem])) {
2303 // Special case when moving from camera preview.
2304 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
2309 private void scaleTo(float scale, int duration) {
2310 if (mViewItem[mCurrentItem] == null) {
2314 mScaleAnimator.setDuration(duration);
2315 mScaleAnimator.setFloatValues(mScale, scale);
2316 mScaleAnimator.start();
2320 public void goToFilmstrip() {
2321 if (mViewItem[mCurrentItem] == null) {
2324 if (mScale == FILM_STRIP_SCALE) {
2327 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
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);
2336 if (mScale == FILM_STRIP_SCALE) {
2342 public void goToFullScreen() {
2343 if (inFullScreen()) {
2347 scaleTo(FULL_SCREEN_SCALE, GEOMETRY_ADJUST_TIME_MS);
2350 private void cancelFlingAnimation() {
2351 // Cancels flinging for zoomed images
2352 if (isFlingAnimationRunning()) {
2353 mFlingAnimator.cancel();
2357 private void cancelZoomAnimation() {
2358 if (isZoomAnimationRunning()) {
2359 mZoomAnimator.cancel();
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) {
2371 mViewItem[i].setVisibility(visible ? VISIBLE : INVISIBLE);
2375 private Uri getCurrentUri() {
2376 ViewItem curr = mViewItem[mCurrentItem];
2380 return mDataAdapter.getImageData(curr.getId()).getUri();
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.
2388 private float getCurrentDataMaxScale(boolean allowOverScale) {
2389 ViewItem curr = mViewItem[mCurrentItem];
2391 return FULL_SCREEN_SCALE;
2393 ImageData imageData = mDataAdapter.getImageData(curr.getId());
2394 if (imageData == null || !imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) {
2395 return FULL_SCREEN_SCALE;
2397 float imageWidth = imageData.getWidth();
2398 if (imageData.getRotation() == 90
2399 || imageData.getRotation() == 270) {
2400 imageWidth = imageData.getHeight();
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;
2412 private void loadZoomedImage() {
2413 if (!inZoomView()) {
2416 ViewItem curr = mViewItem[mCurrentItem];
2420 ImageData imageData = mDataAdapter.getImageData(curr.getId());
2421 if (!imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) {
2424 Uri uri = getCurrentUri();
2425 RectF viewRect = curr.getViewRect();
2426 if (uri == null || uri == Uri.EMPTY) {
2429 int orientation = imageData.getRotation();
2430 mZoomView.loadBitmap(uri, orientation, viewRect);
2433 private void cancelLoadingZoomedImage() {
2434 mZoomView.cancelPartialDecodingTask();
2438 public void goToFirstItem() {
2439 if (mViewItem[mCurrentItem] == null) {
2443 // TODO: animate to camera if it is still in the mViewItem buffer
2444 // versus a full reload which will perform an immediate transition
2448 public boolean inZoomView() {
2449 return FilmstripView.this.inZoomView();
2452 public boolean isFlingAnimationRunning() {
2453 return mFlingAnimator != null && mFlingAnimator.isRunning();
2456 public boolean isZoomAnimationRunning() {
2457 return mZoomAnimator != null && mZoomAnimator.isRunning();
2461 private boolean isCurrentItemCentered() {
2462 return mViewItem[mCurrentItem].getCenterX() == mCenterX;
2465 private static class MyScroller {
2466 public interface Listener {
2467 public void onScrollUpdate(int currX, int currY);
2469 public void onScrollEnd();
2472 private final Handler mHandler;
2473 private final Listener mListener;
2475 private final Scroller mScroller;
2477 private final ValueAnimator mXScrollAnimator;
2478 private final Runnable mScrollChecker = new Runnable() {
2481 boolean newPosition = mScroller.computeScrollOffset();
2483 mListener.onScrollEnd();
2486 mListener.onScrollUpdate(mScroller.getCurrX(), mScroller.getCurrY());
2487 mHandler.removeCallbacks(this);
2488 mHandler.post(this);
2492 private final ValueAnimator.AnimatorUpdateListener mXScrollAnimatorUpdateListener =
2493 new ValueAnimator.AnimatorUpdateListener() {
2495 public void onAnimationUpdate(ValueAnimator animation) {
2496 mListener.onScrollUpdate((Integer) animation.getAnimatedValue(), 0);
2500 private final Animator.AnimatorListener mXScrollAnimatorListener =
2501 new Animator.AnimatorListener() {
2503 public void onAnimationCancel(Animator animation) {
2508 public void onAnimationEnd(Animator animation) {
2509 mListener.onScrollEnd();
2513 public void onAnimationRepeat(Animator animation) {
2518 public void onAnimationStart(Animator animation) {
2523 public MyScroller(Context ctx, Handler handler, Listener listener,
2524 TimeInterpolator interpolator) {
2526 mListener = listener;
2527 mScroller = new Scroller(ctx);
2528 mXScrollAnimator = new ValueAnimator();
2529 mXScrollAnimator.addUpdateListener(mXScrollAnimatorUpdateListener);
2530 mXScrollAnimator.addListener(mXScrollAnimatorListener);
2531 mXScrollAnimator.setInterpolator(interpolator);
2535 int startX, int startY,
2536 int velocityX, int velocityY,
2538 int minY, int maxY) {
2539 mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
2543 public void startScroll(int startX, int startY, int dx, int dy) {
2544 mScroller.startScroll(startX, startY, dx, dy);
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();
2556 public boolean isFinished() {
2557 return (mScroller.isFinished() && !mXScrollAnimator.isRunning());
2560 public void forceFinished(boolean finished) {
2561 mScroller.forceFinished(finished);
2563 mXScrollAnimator.cancel();
2567 private void runChecker() {
2568 if (mHandler == null || mListener == null) {
2571 mHandler.removeCallbacks(mScrollChecker);
2572 mHandler.post(mScrollChecker);
2576 private class MyGestureReceiver implements FilmstripGestureRecognizer.Listener {
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;
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();
2596 } else if (inFullScreen()) {
2597 if (mFullScreenUIHidden) {
2598 onLeaveFullScreenUiHidden();
2599 onEnterFullScreen();
2601 onLeaveFullScreen();
2602 onEnterFullScreenUiHidden();
2610 public boolean onDoubleTap(float x, float y) {
2611 ViewItem current = mViewItem[mCurrentItem];
2612 if (current == null) {
2615 if (inFilmstrip()) {
2616 mController.goToFullScreen();
2618 } else if (mScale < FULL_SCREEN_SCALE || inCameraFullscreen()) {
2621 if (!mController.stopScrolling(false)) {
2624 if (inFullScreen()) {
2625 mController.zoomAt(current, x, y);
2626 checkItemAtMaxSize();
2628 } else if (mScale > FULL_SCREEN_SCALE) {
2630 mController.zoomAt(current, x, y);
2636 public boolean onDown(float x, float y) {
2637 mLastDownTime = SystemClock.uptimeMillis();
2639 mController.cancelFlingAnimation();
2640 if (!mController.stopScrolling(false)) {
2648 public boolean onUp(float x, float y) {
2649 ViewItem currItem = mViewItem[mCurrentItem];
2650 if (currItem == null) {
2653 if (mController.isZoomAnimationRunning() || mController.isFlingAnimationRunning()) {
2657 mController.loadZoomedImage();
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) {
2671 float transY = mViewItem[i].getTranslationY();
2675 int id = mViewItem[i].getId();
2677 if (mDataAdapter.getImageData(id)
2678 .isUIActionSupported(ImageData.ACTION_DEMOTE)
2679 && ((transY > promoteHeight)
2680 || (transY > velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) {
2682 } else if (mDataAdapter.getImageData(id)
2683 .isUIActionSupported(ImageData.ACTION_PROMOTE)
2684 && (transY < -promoteHeight
2685 || (transY < -velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) {
2688 // put the view back.
2689 slideViewBack(mViewItem[i]);
2693 // The data might be changed. Re-check.
2694 currItem = mViewItem[mCurrentItem];
2695 if (currItem == null) {
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);
2713 if (isCurrentItemCentered() && currId == 0 && isViewTypeSticky(currItem)) {
2714 mController.goToFullScreen();
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
2720 mController.goToFilmstrip();
2721 mDataIdOnUserScrolling = currId;
2729 public void onLongPress(float x, float y) {
2730 final int dataId = getCurrentId();
2734 mListener.onFocusedDataLongPressed(dataId);
2738 public boolean onScroll(float x, float y, float dx, float dy) {
2739 final ViewItem currItem = mViewItem[mCurrentItem];
2740 if (currItem == null) {
2743 if (inFullScreen() && !mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
2747 // When image is zoomed in to be bigger than the screen
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());
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();
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;
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;
2776 mController.scroll(deltaX);
2778 // Vertical part. Promote or demote.
2780 Rect hitRect = new Rect();
2781 for (; hit < BUFFER_SIZE; hit++) {
2782 if (mViewItem[hit] == null) {
2785 mViewItem[hit].getHitRect(hitRect);
2786 if (hitRect.contains((int) x, (int) y)) {
2790 if (hit == BUFFER_SIZE) {
2795 ImageData data = mDataAdapter.getImageData(mViewItem[hit].getId());
2796 float transY = mViewItem[hit].getTranslationY() - dy / mScale;
2797 if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) &&
2801 if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) &&
2805 mViewItem[hit].setTranslationY(transY);
2807 } else if (inFullScreen()) {
2808 if (mViewItem[mCurrentItem] == null || (deltaX < 0 && mCenterX <=
2809 currItem.getCenterX() && currItem.getId() == 0)) {
2812 // Multiplied by 1.2 to make it more easy to swipe.
2813 mController.scroll((int) (deltaX * 1.2));
2821 public boolean onMouseScroll(float hscroll, float vscroll) {
2824 hscroll *= MOUSE_SCROLL_FACTOR;
2825 vscroll *= MOUSE_SCROLL_FACTOR;
2827 if (vscroll != 0f) {
2833 if (inFullScreen()) {
2834 onFling(-scroll, 0f);
2835 } else if (inZoomView()) {
2836 onScroll(0f, 0f, hscroll, vscroll);
2838 onScroll(0f, 0f, scroll, 0f);
2845 public boolean onFling(float velocityX, float velocityY) {
2846 final ViewItem currItem = mViewItem[mCurrentItem];
2847 if (currItem == null) {
2850 if (!mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
2854 // Fling within the zoomed image
2855 mController.flingInsideZoomView(velocityX, velocityY);
2858 if (Math.abs(velocityX) < Math.abs(velocityY)) {
2859 // ignore vertical fling.
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();
2868 if (velocityX > 0) { // left
2869 if (mCenterX > currItemCenterX) {
2870 // The visually previous item is actually the current
2872 mController.scrollToPosition(
2873 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
2876 ViewItem prevItem = mViewItem[mCurrentItem - 1];
2877 if (prevItem == null) {
2880 mController.scrollToPosition(
2881 prevItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true);
2883 if (mController.stopScrolling(false)) {
2884 if (mCenterX < currItemCenterX) {
2885 // The visually next item is actually the current
2887 mController.scrollToPosition(
2888 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
2891 final ViewItem nextItem = mViewItem[mCurrentItem + 1];
2892 if (nextItem == null) {
2895 mController.scrollToPosition(
2896 nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true);
2897 if (isViewTypeSticky(currItem)) {
2898 mController.goToFilmstrip();
2904 if (mScale == FILM_STRIP_SCALE) {
2905 mController.fling(velocityX);
2911 public boolean onScaleBegin(float focusX, float focusY) {
2912 if (inCameraFullscreen()) {
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);
2925 public boolean onScale(float focusX, float focusY, float scale) {
2926 if (inCameraFullscreen()) {
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;
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) {
2942 if (newScale == FILM_STRIP_SCALE) {
2948 } else if (mScale < FULL_SCREEN_SCALE && newScale >= FULL_SCREEN_SCALE) {
2949 // Going from smaller than screen size to bigger than or equal
2951 if (mScale == FILM_STRIP_SCALE) {
2954 mScale = FULL_SCREEN_SCALE;
2955 onEnterFullScreen();
2956 mController.setSurroundingViewsVisible(false);
2958 } else if (mScale >= FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) {
2959 // Going from bigger than or equal to screen size to smaller
2961 if (inFullScreen()) {
2962 if (mFullScreenUIHidden) {
2963 onLeaveFullScreenUiHidden();
2965 onLeaveFullScreen();
2974 // Scaled view bigger than or equal to screen size both before
2975 // and after scaling
2976 if (!inZoomView()) {
2977 mController.setSurroundingViewsVisible(false);
2979 ViewItem curr = mViewItem[mCurrentItem];
2980 // Make sure the image is not overly scaled
2981 newScale = Math.min(newScale, mMaxScale);
2982 if (newScale == mScale) {
2985 float postScale = newScale / mScale;
2986 curr.postScale(focusX, focusY, postScale, mDrawArea.width(), mDrawArea.height());
2988 if (mScale == FULL_SCREEN_SCALE) {
2989 onEnterFullScreen();
2993 checkItemAtMaxSize();
2999 public void onScaleEnd() {
3000 zoomAtIndexChanged();
3001 if (mScale > FULL_SCREEN_SCALE + TOLERANCE) {
3004 mController.setSurroundingViewsVisible(true);
3005 if (mScale <= FILM_STRIP_SCALE + TOLERANCE) {
3006 mController.goToFilmstrip();
3007 } else if (mScaleTrend > 1f || mScale > FULL_SCREEN_SCALE - TOLERANCE) {
3009 mScale = FULL_SCREEN_SCALE;
3012 mController.goToFullScreen();
3014 mController.goToFilmstrip();