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.content.Context;
25 import android.graphics.Canvas;
26 import android.graphics.Point;
27 import android.graphics.Rect;
28 import android.graphics.RectF;
29 import android.net.Uri;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.os.SystemClock;
34 import android.util.AttributeSet;
35 import android.util.DisplayMetrics;
36 import android.util.SparseArray;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.view.accessibility.AccessibilityNodeInfo;
41 import android.view.animation.DecelerateInterpolator;
42 import android.widget.Scroller;
44 import com.android.camera.CameraActivity;
45 import com.android.camera.debug.Log;
46 import com.android.camera.filmstrip.DataAdapter;
47 import com.android.camera.filmstrip.FilmstripController;
48 import com.android.camera.filmstrip.ImageData;
49 import com.android.camera.ui.FilmstripGestureRecognizer;
50 import com.android.camera.ui.ZoomView;
51 import com.android.camera.util.ApiHelper;
52 import com.android.camera.util.CameraUtil;
53 import com.android.camera2.R;
55 import java.util.ArrayDeque;
56 import java.util.Arrays;
57 import java.util.Queue;
59 public class FilmstripView extends ViewGroup {
60 private static final Log.Tag TAG = new Log.Tag("FilmstripView");
62 private static final int BUFFER_SIZE = 5;
63 private static final int GEOMETRY_ADJUST_TIME_MS = 400;
64 private static final int SNAP_IN_CENTER_TIME_MS = 600;
65 private static final float FLING_COASTING_DURATION_S = 0.05f;
66 private static final int ZOOM_ANIMATION_DURATION_MS = 200;
67 private static final int CAMERA_PREVIEW_SWIPE_THRESHOLD = 300;
68 private static final float FILM_STRIP_SCALE = 0.7f;
69 private static final float FULL_SCREEN_SCALE = 1f;
71 // The min velocity at which the user must have moved their finger in
72 // pixels per millisecond to count a vertical gesture as a promote/demote
73 // at short vertical distances.
74 private static final float PROMOTE_VELOCITY = 3.5f;
75 // The min distance relative to this view's height the user must have
76 // moved their finger to count a vertical gesture as a promote/demote if
77 // they moved their finger at least at PROMOTE_VELOCITY.
78 private static final float VELOCITY_PROMOTE_HEIGHT_RATIO = 1/10f;
79 // The min distance relative to this view's height the user must have
80 // moved their finger to count a vertical gesture as a promote/demote if
81 // they moved their finger at less than PROMOTE_VELOCITY.
82 private static final float PROMOTE_HEIGHT_RATIO = 1/2f;
84 private static final float TOLERANCE = 0.1f;
85 // Only check for intercepting touch events within first 500ms
86 private static final int SWIPE_TIME_OUT = 500;
87 private static final int DECELERATION_FACTOR = 4;
89 private CameraActivity mActivity;
90 private FilmstripGestureRecognizer mGestureRecognizer;
91 private FilmstripGestureRecognizer.Listener mGestureListener;
92 private DataAdapter mDataAdapter;
93 private int mViewGapInPixel;
94 private final Rect mDrawArea = new Rect();
96 private final int mCurrentItem = (BUFFER_SIZE - 1) / 2;
98 private MyController mController;
99 private int mCenterX = -1;
100 private final ViewItem[] mViewItem = new ViewItem[BUFFER_SIZE];
102 private FilmstripController.FilmstripListener mListener;
103 private ZoomView mZoomView = null;
105 private MotionEvent mDown;
106 private boolean mCheckToIntercept = true;
108 private TimeInterpolator mViewAnimInterpolator;
110 // This is true if and only if the user is scrolling,
111 private boolean mIsUserScrolling;
112 private int mDataIdOnUserScrolling;
113 private float mOverScaleFactor = 1f;
115 private boolean mFullScreenUIHidden = false;
116 private final SparseArray<Queue<View>> recycledViews = new SparseArray<Queue<View>>();
119 * A helper class to tract and calculate the view coordination.
121 private class ViewItem {
123 /** The position of the left of the view in the whole filmstrip. */
124 private int mLeftPosition;
125 private final View mView;
126 private final ImageData mData;
127 private final RectF mViewArea;
128 private boolean mMaximumBitmapRequested;
130 private ValueAnimator mTranslationXAnimator;
131 private ValueAnimator mTranslationYAnimator;
132 private ValueAnimator mAlphaAnimator;
137 * @param id The id of the data from
138 * {@link com.android.camera.filmstrip.DataAdapter}.
139 * @param v The {@code View} representing the data.
141 public ViewItem(int id, View v, ImageData data) {
147 mMaximumBitmapRequested = false;
149 mViewArea = new RectF();
152 public boolean isMaximumBitmapRequested() {
153 return mMaximumBitmapRequested;
156 public void setMaximumBitmapRequested() {
157 mMaximumBitmapRequested = true;
161 * Returns the data id from
162 * {@link com.android.camera.filmstrip.DataAdapter}.
169 * Sets the data id from
170 * {@link com.android.camera.filmstrip.DataAdapter}.
172 public void setId(int id) {
176 /** Sets the left position of the view in the whole filmstrip. */
177 public void setLeftPosition(int pos) {
181 /** Returns the left position of the view in the whole filmstrip. */
182 public int getLeftPosition() {
183 return mLeftPosition;
186 /** Returns the translation of Y regarding the view scale. */
187 public float getTranslationY() {
188 return mView.getTranslationY() / mScale;
191 /** Returns the translation of X regarding the view scale. */
192 public float getTranslationX() {
193 return mView.getTranslationX() / mScale;
196 /** Sets the translation of Y regarding the view scale. */
197 public void setTranslationY(float transY) {
198 mView.setTranslationY(transY * mScale);
201 /** Sets the translation of X regarding the view scale. */
202 public void setTranslationX(float transX) {
203 mView.setTranslationX(transX * mScale);
206 /** Forwarding of {@link android.view.View#setAlpha(float)}. */
207 public void setAlpha(float alpha) {
208 mView.setAlpha(alpha);
211 /** Forwarding of {@link android.view.View#getAlpha()}. */
212 public float getAlpha() {
213 return mView.getAlpha();
216 /** Forwarding of {@link android.view.View#getMeasuredWidth()}. */
217 public int getMeasuredWidth() {
218 return mView.getMeasuredWidth();
222 * Animates the X translation of the view. Note: the animated value is
223 * not set directly by {@link android.view.View#setTranslationX(float)}
224 * because the value might be changed during in {@code onLayout()}.
225 * The animated value of X translation is specially handled in {@code
228 * @param targetX The final value.
229 * @param duration_ms The duration of the animation.
230 * @param interpolator Time interpolator.
232 public void animateTranslationX(
233 float targetX, long duration_ms, TimeInterpolator interpolator) {
234 if (mTranslationXAnimator == null) {
235 mTranslationXAnimator = new ValueAnimator();
236 mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
238 public void onAnimationUpdate(ValueAnimator valueAnimator) {
239 // We invalidate the filmstrip view instead of setting the
240 // translation X because the translation X of the view is
241 // touched in onLayout(). See the documentation of
242 // animateTranslationX().
247 runAnimation(mTranslationXAnimator, getTranslationX(), targetX, duration_ms,
252 * Animates the Y translation of the view.
254 * @param targetY The final value.
255 * @param duration_ms The duration of the animation.
256 * @param interpolator Time interpolator.
258 public void animateTranslationY(
259 float targetY, long duration_ms, TimeInterpolator interpolator) {
260 if (mTranslationYAnimator == null) {
261 mTranslationYAnimator = new ValueAnimator();
262 mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
264 public void onAnimationUpdate(ValueAnimator valueAnimator) {
265 setTranslationY((Float) valueAnimator.getAnimatedValue());
269 runAnimation(mTranslationYAnimator, getTranslationY(), targetY, duration_ms,
274 * Animates the alpha value of the view.
276 * @param targetAlpha The final value.
277 * @param duration_ms The duration of the animation.
278 * @param interpolator Time interpolator.
280 public void animateAlpha(float targetAlpha, long duration_ms,
281 TimeInterpolator interpolator) {
282 if (mAlphaAnimator == null) {
283 mAlphaAnimator = new ValueAnimator();
284 mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
286 public void onAnimationUpdate(ValueAnimator valueAnimator) {
287 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue());
291 runAnimation(mAlphaAnimator, getAlpha(), targetAlpha, duration_ms, interpolator);
294 private void runAnimation(final ValueAnimator animator, final float startValue,
295 final float targetValue, final long duration_ms,
296 final TimeInterpolator interpolator) {
297 if (startValue == targetValue) {
300 animator.setInterpolator(interpolator);
301 animator.setDuration(duration_ms);
302 animator.setFloatValues(startValue, targetValue);
306 /** Adjusts the translation of X regarding the view scale. */
307 public void translateXScaledBy(float transX) {
308 setTranslationX(getTranslationX() + transX * mScale);
312 * Forwarding of {@link android.view.View#getHitRect(android.graphics.Rect)}.
314 public void getHitRect(Rect rect) {
315 mView.getHitRect(rect);
318 public int getCenterX() {
319 return mLeftPosition + mView.getMeasuredWidth() / 2;
322 /** Forwarding of {@link android.view.View#getVisibility()}. */
323 public int getVisibility() {
324 return mView.getVisibility();
327 /** Forwarding of {@link android.view.View#setVisibility(int)}. */
328 public void setVisibility(int visibility) {
329 mView.setVisibility(visibility);
333 * Notifies the {@link com.android.camera.filmstrip.DataAdapter} to
336 public void resizeView(Context context, int w, int h) {
337 mDataAdapter.resizeView(context, mDataId, mView, w, h);
341 * Adds the view of the data to the view hierarchy if necessary.
343 public void addViewToHierarchy() {
344 if (indexOfChild(mView) < 0) {
349 setVisibility(View.VISIBLE);
356 * Removes from the hierarchy. Keeps the view in the view hierarchy if
357 * view type is {@code VIEW_TYPE_STICKY} and set to invisible instead.
359 * @param force {@code true} to remove the view from the hierarchy
360 * regardless of the view type.
362 public void removeViewFromHierarchy(boolean force) {
363 if (force || mData.getViewType() != ImageData.VIEW_TYPE_STICKY) {
365 mData.recycle(mView);
366 recycleView(mView, mDataId);
368 setVisibility(View.INVISIBLE);
373 * Brings the view to front by
374 * {@link #bringChildToFront(android.view.View)}
376 public void bringViewToFront() {
377 bringChildToFront(mView);
381 * The visual x position of this view, in pixels.
383 public float getX() {
388 * The visual y position of this view, in pixels.
390 public float getY() {
395 * Forwarding of {@link android.view.View#measure(int, int)}.
397 public void measure(int widthSpec, int heightSpec) {
398 mView.measure(widthSpec, heightSpec);
401 private void layoutAt(int left, int top) {
402 mView.layout(left, top, left + mView.getMeasuredWidth(),
403 top + mView.getMeasuredHeight());
407 * The bounding rect of the view.
409 public RectF getViewRect() {
410 RectF r = new RectF();
411 r.left = mView.getX();
412 r.top = mView.getY();
413 r.right = r.left + mView.getWidth() * mView.getScaleX();
414 r.bottom = r.top + mView.getHeight() * mView.getScaleY();
418 private View getView() {
423 * Layouts the view in the area assuming the center of the area is at a
424 * specific point of the whole filmstrip.
426 * @param drawArea The area when filmstrip will show in.
427 * @param refCenter The absolute X coordination in the whole filmstrip
428 * of the center of {@code drawArea}.
429 * @param scale The scale of the view on the filmstrip.
431 public void layoutWithTranslationX(Rect drawArea, int refCenter, float scale) {
432 final float translationX =
433 ((mTranslationXAnimator != null && mTranslationXAnimator.isRunning()) ?
434 (Float) mTranslationXAnimator.getAnimatedValue() : 0);
436 (int) (drawArea.centerX() + (mLeftPosition - refCenter + translationX) * scale);
437 int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale);
439 mView.setScaleX(scale);
440 mView.setScaleY(scale);
442 // update mViewArea for touch detection.
443 int l = mView.getLeft();
444 int t = mView.getTop();
446 l + mView.getMeasuredWidth() * scale,
447 t + mView.getMeasuredHeight() * scale);
450 /** Returns true if the point is in the view. */
451 public boolean areaContains(float x, float y) {
452 return mViewArea.contains(x, y);
456 * Return the width of the view.
458 public int getWidth() {
459 return mView.getWidth();
463 * Returns the position of the left edge of the view area content is drawn in.
465 public int getDrawAreaLeft() {
466 return Math.round(mViewArea.left);
469 public void copyAttributes(ViewItem item) {
470 setLeftPosition(item.getLeftPosition());
472 setTranslationX(item.getTranslationX());
473 if (item.mTranslationXAnimator != null) {
474 mTranslationXAnimator = item.mTranslationXAnimator;
475 mTranslationXAnimator.removeAllUpdateListeners();
476 mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
478 public void onAnimationUpdate(ValueAnimator valueAnimator) {
479 // We invalidate the filmstrip view instead of setting the
480 // translation X because the translation X of the view is
481 // touched in onLayout(). See the documentation of
482 // animateTranslationX().
488 setTranslationY(item.getTranslationY());
489 if (item.mTranslationYAnimator != null) {
490 mTranslationYAnimator = item.mTranslationYAnimator;
491 mTranslationYAnimator.removeAllUpdateListeners();
492 mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
494 public void onAnimationUpdate(ValueAnimator valueAnimator) {
495 setTranslationY((Float) valueAnimator.getAnimatedValue());
500 setAlpha(item.getAlpha());
501 if (item.mAlphaAnimator != null) {
502 mAlphaAnimator = item.mAlphaAnimator;
503 mAlphaAnimator.removeAllUpdateListeners();
504 mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
506 public void onAnimationUpdate(ValueAnimator valueAnimator) {
507 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue());
514 * Apply a scale factor (i.e. {@code postScale}) on top of current scale at
515 * pivot point ({@code focusX}, {@code focusY}). Visually it should be the
516 * same as post concatenating current view's matrix with specified scale.
518 void postScale(float focusX, float focusY, float postScale, int viewportWidth,
519 int viewportHeight) {
520 float transX = mView.getTranslationX();
521 float transY = mView.getTranslationY();
522 // Pivot point is top left of the view, so we need to translate
523 // to scale around focus point
524 transX -= (focusX - getX()) * (postScale - 1f);
525 transY -= (focusY - getY()) * (postScale - 1f);
526 float scaleX = mView.getScaleX() * postScale;
527 float scaleY = mView.getScaleY() * postScale;
528 updateTransform(transX, transY, scaleX, scaleY, viewportWidth,
532 void updateTransform(float transX, float transY, float scaleX, float scaleY,
533 int viewportWidth, int viewportHeight) {
534 float left = transX + mView.getLeft();
535 float top = transY + mView.getTop();
536 RectF r = ZoomView.adjustToFitInBounds(new RectF(left, top,
537 left + mView.getWidth() * scaleX,
538 top + mView.getHeight() * scaleY),
539 viewportWidth, viewportHeight);
540 mView.setScaleX(scaleX);
541 mView.setScaleY(scaleY);
542 transX = r.left - mView.getLeft();
543 transY = r.top - mView.getTop();
544 mView.setTranslationX(transX);
545 mView.setTranslationY(transY);
548 void resetTransform() {
549 mView.setScaleX(FULL_SCREEN_SCALE);
550 mView.setScaleY(FULL_SCREEN_SCALE);
551 mView.setTranslationX(0f);
552 mView.setTranslationY(0f);
556 public String toString() {
557 return "DataID = " + mDataId + "\n\t left = " + mLeftPosition
558 + "\n\t viewArea = " + mViewArea
559 + "\n\t centerX = " + getCenterX()
560 + "\n\t view MeasuredSize = "
561 + mView.getMeasuredWidth() + ',' + mView.getMeasuredHeight()
562 + "\n\t view Size = " + mView.getWidth() + ',' + mView.getHeight()
563 + "\n\t view scale = " + mView.getScaleX();
568 public FilmstripView(Context context) {
570 init((CameraActivity) context);
574 public FilmstripView(Context context, AttributeSet attrs) {
575 super(context, attrs);
576 init((CameraActivity) context);
580 public FilmstripView(Context context, AttributeSet attrs, int defStyle) {
581 super(context, attrs, defStyle);
582 init((CameraActivity) context);
585 private void init(CameraActivity cameraActivity) {
586 setWillNotDraw(false);
587 mActivity = cameraActivity;
589 mDataIdOnUserScrolling = 0;
590 mController = new MyController(cameraActivity);
591 mViewAnimInterpolator = new DecelerateInterpolator();
592 mZoomView = new ZoomView(cameraActivity);
593 mZoomView.setVisibility(GONE);
596 mGestureListener = new MyGestureReceiver();
598 new FilmstripGestureRecognizer(cameraActivity, mGestureListener);
599 mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop);
600 DisplayMetrics metrics = new DisplayMetrics();
601 mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
602 // Allow over scaling because on high density screens, pixels are too
603 // tiny to clearly see the details at 1:1 zoom. We should not scale
604 // beyond what 1:1 would look like on a medium density screen, as
605 // scaling beyond that would only yield blur.
606 mOverScaleFactor = (float) metrics.densityDpi / (float) DisplayMetrics.DENSITY_HIGH;
607 if (mOverScaleFactor < 1f) {
608 mOverScaleFactor = 1f;
611 setAccessibilityDelegate(new AccessibilityDelegate() {
613 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
614 super.onInitializeAccessibilityNodeInfo(host, info);
616 info.setClassName(FilmstripView.class.getName());
617 info.setScrollable(true);
618 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
619 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
623 public boolean performAccessibilityAction(View host, int action, Bundle args) {
624 if (!mController.isScrolling()) {
626 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
627 mController.goToNextItem();
630 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
631 boolean wentToPrevious = mController.goToPreviousItem();
632 if (!wentToPrevious) {
633 // at beginning of filmstrip, hide and go back to preview
634 mActivity.getCameraAppUI().hideFilmstrip();
638 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
639 // Prevent the view group itself from being selected.
640 // Instead, select the item in the center
641 final ViewItem currentItem = mViewItem[mCurrentItem];
642 currentItem.getView().performAccessibilityAction(action, args);
647 return super.performAccessibilityAction(host, action, args);
652 private void recycleView(View view, int dataId) {
653 final int viewType = (Integer) view.getTag(R.id.mediadata_tag_viewtype);
655 Queue<View> recycledViewsForType = recycledViews.get(viewType);
656 if (recycledViewsForType == null) {
657 recycledViewsForType = new ArrayDeque<View>();
658 recycledViews.put(viewType, recycledViewsForType);
660 recycledViewsForType.offer(view);
664 private View getRecycledView(int dataId) {
665 final int viewType = mDataAdapter.getItemViewType(dataId);
666 Queue<View> recycledViewsForType = recycledViews.get(viewType);
668 if (recycledViewsForType != null) {
669 result = recycledViewsForType.poll();
675 * Returns the controller.
677 * @return The {@code Controller}.
679 public FilmstripController getController() {
684 * Returns the draw area width of the current item.
686 public int getCurrentItemLeft() {
687 return mViewItem[mCurrentItem].getDrawAreaLeft();
690 private void setListener(FilmstripController.FilmstripListener l) {
694 private void setViewGap(int viewGap) {
695 mViewGapInPixel = viewGap;
699 * Called after current item or zoom level has changed.
701 public void zoomAtIndexChanged() {
702 if (mViewItem[mCurrentItem] == null) {
705 int id = mViewItem[mCurrentItem].getId();
706 mListener.onZoomAtIndexChanged(id, mScale);
710 * Checks if the data is at the center.
712 * @param id The id of the data to check.
713 * @return {@code True} if the data is currently at the center.
715 private boolean isDataAtCenter(int id) {
716 if (mViewItem[mCurrentItem] == null) {
719 if (mViewItem[mCurrentItem].getId() == id
720 && isCurrentItemCentered()) {
726 private void measureViewItem(ViewItem item, int boundWidth, int boundHeight) {
727 int id = item.getId();
728 ImageData imageData = mDataAdapter.getImageData(id);
729 if (imageData == null) {
730 Log.e(TAG, "trying to measure a null item");
734 Point dim = CameraUtil.resizeToFill(imageData.getWidth(), imageData.getHeight(),
735 imageData.getRotation(), boundWidth, boundHeight);
737 item.measure(MeasureSpec.makeMeasureSpec(dim.x, MeasureSpec.EXACTLY),
738 MeasureSpec.makeMeasureSpec(dim.y, MeasureSpec.EXACTLY));
742 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
743 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
745 int boundWidth = MeasureSpec.getSize(widthMeasureSpec);
746 int boundHeight = MeasureSpec.getSize(heightMeasureSpec);
747 if (boundWidth == 0 || boundHeight == 0) {
748 // Either width or height is unknown, can't measure children yet.
752 for (ViewItem item : mViewItem) {
754 measureViewItem(item, boundWidth, boundHeight);
759 mZoomView.measure(MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.EXACTLY),
760 MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.EXACTLY));
763 private int findTheNearestView(int pointX) {
766 // Find the first non-null ViewItem.
767 while (nearest < BUFFER_SIZE
768 && (mViewItem[nearest] == null || mViewItem[nearest].getLeftPosition() == -1)) {
771 // No existing available ViewItem
772 if (nearest == BUFFER_SIZE) {
776 int min = Math.abs(pointX - mViewItem[nearest].getCenterX());
778 for (int itemID = nearest + 1; itemID < BUFFER_SIZE && mViewItem[itemID] != null; itemID++) {
780 if (mViewItem[itemID].getLeftPosition() == -1) {
784 int c = mViewItem[itemID].getCenterX();
785 int dist = Math.abs(pointX - c);
794 private ViewItem buildItemFromData(int dataID) {
795 if (mActivity.isDestroyed()) {
796 // Loading item data is call from multiple AsyncTasks and the
797 // activity may be finished when buildItemFromData is called.
798 Log.d(TAG, "Activity destroyed, don't load data");
801 ImageData data = mDataAdapter.getImageData(dataID);
806 // Always scale by fixed filmstrip scale, since we only show items when
807 // in filmstrip. Preloading images with a different scale and bounds
808 // interferes with caching.
809 int width = Math.round(FILM_STRIP_SCALE * getWidth());
810 int height = Math.round(FILM_STRIP_SCALE * getHeight());
811 Log.v(TAG, "suggesting item bounds: " + width + "x" + height);
812 mDataAdapter.suggestViewSizeBound(width, height);
815 View recycled = getRecycledView(dataID);
816 View v = mDataAdapter.getView(mActivity, recycled, dataID);
820 ViewItem item = new ViewItem(dataID, v, data);
821 item.addViewToHierarchy();
825 private void checkItemAtMaxSize() {
826 ViewItem item = mViewItem[mCurrentItem];
827 if (item.isMaximumBitmapRequested()) {
830 item.setMaximumBitmapRequested();
831 // Request full size bitmap, or max that DataAdapter will create.
832 int id = item.getId();
833 int h = mDataAdapter.getImageData(id).getHeight();
834 int w = mDataAdapter.getImageData(id).getWidth();
835 item.resizeView(mActivity, w, h);
838 private void removeItem(int itemID) {
839 if (itemID >= mViewItem.length || mViewItem[itemID] == null) {
842 ImageData data = mDataAdapter.getImageData(mViewItem[itemID].getId());
844 Log.e(TAG, "trying to remove a null item");
847 mViewItem[itemID].removeViewFromHierarchy(false);
848 mViewItem[itemID] = null;
852 * We try to keep the one closest to the center of the screen at position
855 private void stepIfNeeded() {
856 if (!inFilmstrip() && !inFullScreen()) {
857 // The good timing to step to the next view is when everything is
858 // not in transition.
861 final int nearest = findTheNearestView(mCenterX);
863 if (nearest == -1 || nearest == mCurrentItem) {
866 int prevDataId = (mViewItem[mCurrentItem] == null ? -1 : mViewItem[mCurrentItem].getId());
867 final int adjust = nearest - mCurrentItem;
869 for (int k = 0; k < adjust; k++) {
872 for (int k = 0; k + adjust < BUFFER_SIZE; k++) {
873 mViewItem[k] = mViewItem[k + adjust];
875 for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) {
877 if (mViewItem[k - 1] != null) {
878 mViewItem[k] = buildItemFromData(mViewItem[k - 1].getId() + 1);
883 for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) {
886 for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) {
887 mViewItem[k] = mViewItem[k + adjust];
889 for (int k = -1 - adjust; k >= 0; k--) {
891 if (mViewItem[k + 1] != null) {
892 mViewItem[k] = buildItemFromData(mViewItem[k + 1].getId() - 1);
897 if (mListener != null) {
898 mListener.onDataFocusChanged(prevDataId, mViewItem[mCurrentItem].getId());
899 final int firstVisible = mViewItem[mCurrentItem].getId() - 2;
900 final int visibleItemCount = firstVisible + BUFFER_SIZE;
901 final int totalItemCount = mDataAdapter.getTotalNumber();
902 mListener.onScroll(firstVisible, visibleItemCount, totalItemCount);
904 zoomAtIndexChanged();
908 * Check the bounds of {@code mCenterX}. Always call this function after: 1.
909 * Any changes to {@code mCenterX}. 2. Any size change of the view items.
911 * @return Whether clamp happened.
913 private boolean clampCenterX() {
914 ViewItem curr = mViewItem[mCurrentItem];
919 boolean stopScroll = false;
920 if (curr.getId() == 1 && mCenterX < curr.getCenterX() && mDataIdOnUserScrolling > 1 &&
921 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY &&
922 mController.isScrolling()) {
925 if (curr.getId() == 0 && mCenterX < curr.getCenterX()) {
926 // Stop at the first ViewItem.
930 if (curr.getId() == mDataAdapter.getTotalNumber() - 1
931 && mCenterX > curr.getCenterX()) {
937 mCenterX = curr.getCenterX();
944 * Reorders the child views to be consistent with their data ID. This method
945 * should be called after adding/removing views.
947 private void adjustChildZOrder() {
948 for (int i = BUFFER_SIZE - 1; i >= 0; i--) {
949 if (mViewItem[i] == null) {
952 mViewItem[i].bringViewToFront();
954 // ZoomView is a special case to always be in the front. In L set to
955 // max elevation to make sure ZoomView is above other elevated views.
956 bringChildToFront(mZoomView);
957 if (ApiHelper.isLOrHigher()) {
958 setMaxElevation(mZoomView);
962 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
963 private void setMaxElevation(View v) {
964 v.setElevation(Float.MAX_VALUE);
968 * Returns the ID of the current item, or -1 if there is no data.
970 private int getCurrentId() {
971 ViewItem current = mViewItem[mCurrentItem];
972 if (current == null) {
975 return current.getId();
979 * Keep the current item in the center. This functions does not check if the
980 * current item is null.
982 private void snapInCenter() {
983 final ViewItem currItem = mViewItem[mCurrentItem];
984 if (currItem == null) {
987 final int currentViewCenter = currItem.getCenterX();
988 if (mController.isScrolling() || mIsUserScrolling
989 || isCurrentItemCentered()) {
993 int snapInTime = (int) (SNAP_IN_CENTER_TIME_MS
994 * ((float) Math.abs(mCenterX - currentViewCenter))
995 / mDrawArea.width());
996 mController.scrollToPosition(currentViewCenter,
998 if (isViewTypeSticky(currItem) && !mController.isScaling() && mScale != FULL_SCREEN_SCALE) {
999 // Now going to full screen camera
1000 mController.goToFullScreen();
1005 * Translates the {@link ViewItem} on the left of the current one to match
1006 * the full-screen layout. In full-screen, we show only one {@link ViewItem}
1007 * which occupies the whole screen. The other left ones are put on the left
1008 * side in full scales. Does nothing if there's no next item.
1010 * @param currItem The item ID of the current one to be translated.
1011 * @param drawAreaWidth The width of the current draw area.
1012 * @param scaleFraction A {@code float} between 0 and 1. 0 if the current
1013 * scale is {@code FILM_STRIP_SCALE}. 1 if the current scale is
1014 * {@code FULL_SCREEN_SCALE}.
1016 private void translateLeftViewItem(
1017 int currItem, int drawAreaWidth, float scaleFraction) {
1018 if (currItem < 0 || currItem > BUFFER_SIZE - 1) {
1019 Log.e(TAG, "currItem id out of bound.");
1023 final ViewItem curr = mViewItem[currItem];
1024 final ViewItem next = mViewItem[currItem + 1];
1025 if (curr == null || next == null) {
1026 Log.e(TAG, "Invalid view item (curr or next == null). curr = "
1031 final int currCenterX = curr.getCenterX();
1032 final int nextCenterX = next.getCenterX();
1033 final int translate = (int) ((nextCenterX - drawAreaWidth
1034 - currCenterX) * scaleFraction);
1036 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1038 curr.setVisibility(VISIBLE);
1040 if (inFullScreen()) {
1041 curr.setTranslationX(translate * (mCenterX - currCenterX) / (nextCenterX - currCenterX));
1043 curr.setTranslationX(translate);
1048 * Fade out the {@link ViewItem} on the right of the current one in
1049 * full-screen layout. Does nothing if there's no previous item.
1051 * @param currItemId The ID of the item to fade.
1053 private void fadeAndScaleRightViewItem(int currItemId) {
1054 if (currItemId < 1 || currItemId > BUFFER_SIZE) {
1055 Log.e(TAG, "currItem id out of bound.");
1059 final ViewItem currItem = mViewItem[currItemId];
1060 final ViewItem prevItem = mViewItem[currItemId - 1];
1061 if (currItem == null || prevItem == null) {
1062 Log.e(TAG, "Invalid view item (curr or prev == null). curr = "
1067 if (currItemId > mCurrentItem + 1) {
1068 // Every item not right next to the mCurrentItem is invisible.
1069 currItem.setVisibility(INVISIBLE);
1072 final int prevCenterX = prevItem.getCenterX();
1073 if (mCenterX <= prevCenterX) {
1074 // Shortcut. If the position is at the center of the previous one,
1075 // set to invisible too.
1076 currItem.setVisibility(INVISIBLE);
1079 final int currCenterX = currItem.getCenterX();
1080 final float fadeDownFraction =
1081 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX);
1082 currItem.layoutWithTranslationX(mDrawArea, currCenterX,
1083 FILM_STRIP_SCALE + (1f - FILM_STRIP_SCALE) * fadeDownFraction);
1084 currItem.setAlpha(fadeDownFraction);
1085 currItem.setTranslationX(0);
1086 currItem.setVisibility(VISIBLE);
1089 private void layoutViewItems(boolean layoutChanged) {
1090 if (mViewItem[mCurrentItem] == null ||
1091 mDrawArea.width() == 0 ||
1092 mDrawArea.height() == 0) {
1096 // If the layout changed, we need to adjust the current position so
1097 // that if an item is centered before the change, it's still centered.
1098 if (layoutChanged) {
1099 mViewItem[mCurrentItem].setLeftPosition(
1100 mCenterX - mViewItem[mCurrentItem].getMeasuredWidth() / 2);
1107 * Transformed scale fraction between 0 and 1. 0 if the scale is
1108 * {@link FILM_STRIP_SCALE}. 1 if the scale is {@link FULL_SCREEN_SCALE}
1111 final float scaleFraction = mViewAnimInterpolator.getInterpolation(
1112 (mScale - FILM_STRIP_SCALE) / (FULL_SCREEN_SCALE - FILM_STRIP_SCALE));
1113 final int fullScreenWidth = mDrawArea.width() + mViewGapInPixel;
1115 // Decide the position for all view items on the left and the right
1119 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
1120 final ViewItem curr = mViewItem[itemID];
1125 // First, layout relatively to the next one.
1126 final int currLeft = mViewItem[itemID + 1].getLeftPosition()
1127 - curr.getMeasuredWidth() - mViewGapInPixel;
1128 curr.setLeftPosition(currLeft);
1131 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
1132 final ViewItem curr = mViewItem[itemID];
1137 // First, layout relatively to the previous one.
1138 final ViewItem prev = mViewItem[itemID - 1];
1139 final int currLeft =
1140 prev.getLeftPosition() + prev.getMeasuredWidth()
1142 curr.setLeftPosition(currLeft);
1145 // Special case for the one immediately on the right of the camera
1147 boolean immediateRight =
1148 (mViewItem[mCurrentItem].getId() == 1 &&
1149 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY);
1151 // Layout the current ViewItem first.
1152 if (immediateRight) {
1153 // Just do a simple layout without any special translation or
1154 // fading. The implementation in Gallery does not push the first
1155 // photo to the bottom of the camera preview. Simply place the
1156 // photo on the right of the preview.
1157 final ViewItem currItem = mViewItem[mCurrentItem];
1158 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1159 currItem.setTranslationX(0f);
1160 currItem.setAlpha(1f);
1161 } else if (scaleFraction == 1f) {
1162 final ViewItem currItem = mViewItem[mCurrentItem];
1163 final int currCenterX = currItem.getCenterX();
1164 if (mCenterX < currCenterX) {
1165 // In full-screen and mCenterX is on the left of the center,
1166 // we draw the current one to "fade down".
1167 fadeAndScaleRightViewItem(mCurrentItem);
1168 } else if (mCenterX > currCenterX) {
1169 // In full-screen and mCenterX is on the right of the center,
1170 // we draw the current one translated.
1171 translateLeftViewItem(mCurrentItem, fullScreenWidth, scaleFraction);
1173 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1174 currItem.setTranslationX(0f);
1175 currItem.setAlpha(1f);
1178 final ViewItem currItem = mViewItem[mCurrentItem];
1179 // The normal filmstrip has no translation for the current item. If
1180 // it has translation before, gradually set it to zero.
1181 currItem.setTranslationX(currItem.getTranslationX() * scaleFraction);
1182 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1183 if (mViewItem[mCurrentItem - 1] == null) {
1184 currItem.setAlpha(1f);
1186 final int currCenterX = currItem.getCenterX();
1187 final int prevCenterX = mViewItem[mCurrentItem - 1].getCenterX();
1188 final float fadeDownFraction =
1189 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX);
1191 (1 - fadeDownFraction) * (1 - scaleFraction) + fadeDownFraction);
1195 // Layout the rest dependent on the current scale.
1197 // Items on the left
1198 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
1199 final ViewItem curr = mViewItem[itemID];
1203 translateLeftViewItem(itemID, fullScreenWidth, scaleFraction);
1206 // Items on the right
1207 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
1208 final ViewItem curr = mViewItem[itemID];
1213 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1214 if (curr.getId() == 1 && isViewTypeSticky(curr)) {
1215 // Special case for the one next to the camera preview.
1220 if (scaleFraction == 1) {
1221 // It's in full-screen mode.
1222 fadeAndScaleRightViewItem(itemID);
1224 if (curr.getVisibility() == INVISIBLE) {
1225 curr.setVisibility(VISIBLE);
1227 if (itemID == mCurrentItem + 1) {
1228 curr.setAlpha(1f - scaleFraction);
1230 if (scaleFraction == 0f) {
1233 curr.setVisibility(INVISIBLE);
1236 curr.setTranslationX(
1237 (mViewItem[mCurrentItem].getLeftPosition() - curr.getLeftPosition()) *
1245 private boolean isViewTypeSticky(ViewItem item) {
1249 return mDataAdapter.getImageData(item.getId()).getViewType() ==
1250 ImageData.VIEW_TYPE_STICKY;
1254 public void onDraw(Canvas c) {
1255 // TODO: remove layoutViewItems() here.
1256 layoutViewItems(false);
1261 protected void onLayout(boolean changed, int l, int t, int r, int b) {
1264 mDrawArea.right = r - l;
1265 mDrawArea.bottom = b - t;
1266 mZoomView.layout(mDrawArea.left, mDrawArea.top, mDrawArea.right, mDrawArea.bottom);
1267 // TODO: Need a more robust solution to decide when to re-layout
1268 // If in the middle of zooming, only re-layout when the layout has
1270 if (!inZoomView() || changed) {
1272 layoutViewItems(changed);
1277 * Clears the translation and scale that has been set on the view, cancels
1278 * any loading request for image partial decoding, and hides zoom view. This
1279 * is needed for when there is a layout change (e.g. when users re-enter the
1280 * app, or rotate the device, etc).
1282 private void resetZoomView() {
1283 if (!inZoomView()) {
1286 ViewItem current = mViewItem[mCurrentItem];
1287 if (current == null) {
1290 mScale = FULL_SCREEN_SCALE;
1291 mController.cancelZoomAnimation();
1292 mController.cancelFlingAnimation();
1293 current.resetTransform();
1294 mController.cancelLoadingZoomedImage();
1295 mZoomView.setVisibility(GONE);
1296 mController.setSurroundingViewsVisible(true);
1299 private void hideZoomView() {
1301 mController.cancelLoadingZoomedImage();
1302 mZoomView.setVisibility(GONE);
1306 private void slideViewBack(ViewItem item) {
1307 item.animateTranslationX(0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1308 item.animateTranslationY(0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1309 item.animateAlpha(1f, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1312 private void animateItemRemoval(int dataID, final ImageData data) {
1313 if (mScale > FULL_SCREEN_SCALE) {
1316 int removedItemId = findItemByDataID(dataID);
1318 // adjust the data id to be consistent
1319 for (int i = 0; i < BUFFER_SIZE; i++) {
1320 if (mViewItem[i] == null || mViewItem[i].getId() <= dataID) {
1323 mViewItem[i].setId(mViewItem[i].getId() - 1);
1325 if (removedItemId == -1) {
1329 final ViewItem removedItem = mViewItem[removedItemId];
1330 final int offsetX = removedItem.getMeasuredWidth() + mViewGapInPixel;
1332 for (int i = removedItemId + 1; i < BUFFER_SIZE; i++) {
1333 if (mViewItem[i] != null) {
1334 mViewItem[i].setLeftPosition(mViewItem[i].getLeftPosition() - offsetX);
1338 if (removedItemId >= mCurrentItem
1339 && mViewItem[removedItemId].getId() < mDataAdapter.getTotalNumber()) {
1340 // Fill the removed item by left shift when the current one or
1341 // anyone on the right is removed, and there's more data on the
1343 for (int i = removedItemId; i < BUFFER_SIZE - 1; i++) {
1344 mViewItem[i] = mViewItem[i + 1];
1347 // pull data out from the DataAdapter for the last one.
1348 int curr = BUFFER_SIZE - 1;
1349 int prev = curr - 1;
1350 if (mViewItem[prev] != null) {
1351 mViewItem[curr] = buildItemFromData(mViewItem[prev].getId() + 1);
1354 // The animation part.
1355 if (inFullScreen()) {
1356 mViewItem[mCurrentItem].setVisibility(VISIBLE);
1357 ViewItem nextItem = mViewItem[mCurrentItem + 1];
1358 if (nextItem != null) {
1359 nextItem.setVisibility(INVISIBLE);
1363 // Translate the views to their original places.
1364 for (int i = removedItemId; i < BUFFER_SIZE; i++) {
1365 if (mViewItem[i] != null) {
1366 mViewItem[i].setTranslationX(offsetX);
1370 // The end of the filmstrip might have been changed.
1371 // The mCenterX might be out of the bound.
1372 ViewItem currItem = mViewItem[mCurrentItem];
1373 if(currItem!=null) {
1374 if (currItem.getId() == mDataAdapter.getTotalNumber() - 1
1375 && mCenterX > currItem.getCenterX()) {
1376 int adjustDiff = currItem.getCenterX() - mCenterX;
1377 mCenterX = currItem.getCenterX();
1378 for (int i = 0; i < BUFFER_SIZE; i++) {
1379 if (mViewItem[i] != null) {
1380 mViewItem[i].translateXScaledBy(adjustDiff);
1385 // CurrItem should NOT be NULL, but if is, at least don't crash.
1386 Log.w(TAG,"Caught invalid update in removal animation.");
1389 // fill the removed place by right shift
1390 mCenterX -= offsetX;
1392 for (int i = removedItemId; i > 0; i--) {
1393 mViewItem[i] = mViewItem[i - 1];
1396 // pull data out from the DataAdapter for the first one.
1398 int next = curr + 1;
1399 if (mViewItem[next] != null) {
1400 mViewItem[curr] = buildItemFromData(mViewItem[next].getId() - 1);
1403 // Translate the views to their original places.
1404 for (int i = removedItemId; i >= 0; i--) {
1405 if (mViewItem[i] != null) {
1406 mViewItem[i].setTranslationX(-offsetX);
1411 int transY = getHeight() / 8;
1412 if (removedItem.getTranslationY() < 0) {
1415 removedItem.animateTranslationY(removedItem.getTranslationY() + transY,
1416 GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1417 removedItem.animateAlpha(0f, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1418 postDelayed(new Runnable() {
1421 removedItem.removeViewFromHierarchy(false);
1423 }, GEOMETRY_ADJUST_TIME_MS);
1425 adjustChildZOrder();
1428 // Now, slide every one back.
1429 if (mViewItem[mCurrentItem] == null) {
1432 for (int i = 0; i < BUFFER_SIZE; i++) {
1433 if (mViewItem[i] != null
1434 && mViewItem[i].getTranslationX() != 0f) {
1435 slideViewBack(mViewItem[i]);
1438 if (isCurrentItemCentered() && isViewTypeSticky(mViewItem[mCurrentItem])) {
1439 // Special case for scrolling onto the camera preview after removal.
1440 mController.goToFullScreen();
1444 // returns -1 on failure.
1445 private int findItemByDataID(int dataID) {
1446 for (int i = 0; i < BUFFER_SIZE; i++) {
1447 if (mViewItem[i] != null
1448 && mViewItem[i].getId() == dataID) {
1455 private void updateInsertion(int dataID) {
1456 int insertedItemId = findItemByDataID(dataID);
1457 if (insertedItemId == -1) {
1458 // Not in the current item buffers. Check if it's inserted
1460 if (dataID == mDataAdapter.getTotalNumber() - 1) {
1461 int prev = findItemByDataID(dataID - 1);
1462 if (prev >= 0 && prev < BUFFER_SIZE - 1) {
1463 // The previous data is in the buffer and we still
1464 // have room for the inserted data.
1465 insertedItemId = prev + 1;
1470 // adjust the data id to be consistent
1471 for (int i = 0; i < BUFFER_SIZE; i++) {
1472 if (mViewItem[i] == null || mViewItem[i].getId() < dataID) {
1475 mViewItem[i].setId(mViewItem[i].getId() + 1);
1477 if (insertedItemId == -1) {
1481 final ImageData data = mDataAdapter.getImageData(dataID);
1482 Point dim = CameraUtil
1483 .resizeToFill(data.getWidth(), data.getHeight(), data.getRotation(),
1484 getMeasuredWidth(), getMeasuredHeight());
1485 final int offsetX = dim.x + mViewGapInPixel;
1486 ViewItem viewItem = buildItemFromData(dataID);
1488 if (insertedItemId >= mCurrentItem) {
1489 if (insertedItemId == mCurrentItem) {
1490 viewItem.setLeftPosition(mViewItem[mCurrentItem].getLeftPosition());
1492 // Shift right to make rooms for newly inserted item.
1493 removeItem(BUFFER_SIZE - 1);
1494 for (int i = BUFFER_SIZE - 1; i > insertedItemId; i--) {
1495 mViewItem[i] = mViewItem[i - 1];
1496 if (mViewItem[i] != null) {
1497 mViewItem[i].setTranslationX(-offsetX);
1498 slideViewBack(mViewItem[i]);
1502 // Shift left. Put the inserted data on the left instead of the
1505 if (insertedItemId < 0) {
1509 for (int i = 1; i <= insertedItemId; i++) {
1510 if (mViewItem[i] != null) {
1511 mViewItem[i].setTranslationX(offsetX);
1512 slideViewBack(mViewItem[i]);
1513 mViewItem[i - 1] = mViewItem[i];
1518 mViewItem[insertedItemId] = viewItem;
1519 viewItem.setAlpha(0f);
1520 viewItem.setTranslationY(getHeight() / 8);
1521 slideViewBack(viewItem);
1522 adjustChildZOrder();
1526 private void setDataAdapter(DataAdapter adapter) {
1527 mDataAdapter = adapter;
1528 int maxEdge = (int) (Math.max(this.getHeight(), this.getWidth())
1529 * FILM_STRIP_SCALE);
1530 mDataAdapter.suggestViewSizeBound(maxEdge, maxEdge);
1531 mDataAdapter.setListener(new DataAdapter.Listener() {
1533 public void onDataLoaded() {
1538 public void onDataUpdated(DataAdapter.UpdateReporter reporter) {
1543 public void onDataInserted(int dataId, ImageData data) {
1544 if (mViewItem[mCurrentItem] == null) {
1545 // empty now, simply do a reload.
1548 updateInsertion(dataId);
1550 if (mListener != null) {
1551 mListener.onDataFocusChanged(dataId, getCurrentId());
1556 public void onDataRemoved(int dataId, ImageData data) {
1557 animateItemRemoval(dataId, data);
1558 if (mListener != null) {
1559 mListener.onDataFocusChanged(dataId, getCurrentId());
1565 private boolean inFilmstrip() {
1566 return (mScale == FILM_STRIP_SCALE);
1569 private boolean inFullScreen() {
1570 return (mScale == FULL_SCREEN_SCALE);
1573 private boolean inZoomView() {
1574 return (mScale > FULL_SCREEN_SCALE);
1577 private boolean isCameraPreview() {
1578 return isViewTypeSticky(mViewItem[mCurrentItem]);
1581 private boolean inCameraFullscreen() {
1582 return isDataAtCenter(0) && inFullScreen()
1583 && (isViewTypeSticky(mViewItem[mCurrentItem]));
1587 public boolean onInterceptTouchEvent(MotionEvent ev) {
1588 if (mController.isScrolling()) {
1592 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
1593 mCheckToIntercept = true;
1594 mDown = MotionEvent.obtain(ev);
1595 ViewItem viewItem = mViewItem[mCurrentItem];
1596 // Do not intercept touch if swipe is not enabled
1597 if (viewItem != null && !mDataAdapter.canSwipeInFullScreen(viewItem.getId())) {
1598 mCheckToIntercept = false;
1601 } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
1602 // Do not intercept touch once child is in zoom mode
1603 mCheckToIntercept = false;
1606 if (!mCheckToIntercept) {
1609 if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) {
1612 int deltaX = (int) (ev.getX() - mDown.getX());
1613 int deltaY = (int) (ev.getY() - mDown.getY());
1614 if (ev.getActionMasked() == MotionEvent.ACTION_MOVE
1615 && deltaX < mSlop * (-1)) {
1616 // intercept left swipe
1617 if (Math.abs(deltaX) >= Math.abs(deltaY) * 2) {
1626 public boolean onTouchEvent(MotionEvent ev) {
1627 return mGestureRecognizer.onTouchEvent(ev);
1630 FilmstripGestureRecognizer.Listener getGestureListener() {
1631 return mGestureListener;
1634 private void updateViewItem(int itemID) {
1635 ViewItem item = mViewItem[itemID];
1637 Log.e(TAG, "trying to update an null item");
1640 item.removeViewFromHierarchy(true);
1642 ViewItem newItem = buildItemFromData(item.getId());
1643 if (newItem == null) {
1644 Log.e(TAG, "new item is null");
1645 // keep using the old data.
1646 item.addViewToHierarchy();
1649 newItem.copyAttributes(item);
1650 mViewItem[itemID] = newItem;
1651 mZoomView.resetDecoder();
1653 boolean stopScroll = clampCenterX();
1655 mController.stopScrolling(true);
1657 adjustChildZOrder();
1659 if (mListener != null) {
1660 mListener.onDataUpdated(newItem.getId());
1664 /** Some of the data is changed. */
1665 private void update(DataAdapter.UpdateReporter reporter) {
1667 if (mViewItem[mCurrentItem] == null) {
1672 // Check the current one.
1673 ViewItem curr = mViewItem[mCurrentItem];
1674 int dataId = curr.getId();
1675 if (reporter.isDataRemoved(dataId)) {
1679 if (reporter.isDataUpdated(dataId)) {
1680 updateViewItem(mCurrentItem);
1681 final ImageData data = mDataAdapter.getImageData(dataId);
1682 if (!mIsUserScrolling && !mController.isScrolling()) {
1683 // If there is no scrolling at all, adjust mCenterX to place
1684 // the current item at the center.
1685 Point dim = CameraUtil.resizeToFill(data.getWidth(), data.getHeight(),
1686 data.getRotation(), getMeasuredWidth(), getMeasuredHeight());
1687 mCenterX = curr.getLeftPosition() + dim.x / 2;
1692 for (int i = mCurrentItem - 1; i >= 0; i--) {
1693 curr = mViewItem[i];
1695 dataId = curr.getId();
1696 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
1700 ViewItem next = mViewItem[i + 1];
1702 mViewItem[i] = buildItemFromData(next.getId() - 1);
1708 for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) {
1709 curr = mViewItem[i];
1711 dataId = curr.getId();
1712 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
1716 ViewItem prev = mViewItem[i - 1];
1718 mViewItem[i] = buildItemFromData(prev.getId() + 1);
1722 adjustChildZOrder();
1723 // Request a layout to find the measured width/height of the view first.
1725 // Update photo sphere visibility after metadata fully written.
1729 * The whole data might be totally different. Flush all and load from the
1730 * start. Filmstrip will be centered on the first item, i.e. the camera
1733 private void reload() {
1734 mController.stopScrolling(true);
1735 mController.stopScale();
1736 mDataIdOnUserScrolling = 0;
1739 if (mViewItem[mCurrentItem] != null) {
1740 prevId = mViewItem[mCurrentItem].getId();
1743 // Remove all views from the mViewItem buffer, except the camera view.
1744 for (int i = 0; i < mViewItem.length; i++) {
1745 if (mViewItem[i] == null) {
1748 mViewItem[i].removeViewFromHierarchy(false);
1751 // Clear out the mViewItems and rebuild with camera in the center.
1752 Arrays.fill(mViewItem, null);
1753 int dataNumber = mDataAdapter.getTotalNumber();
1754 if (dataNumber == 0) {
1758 mViewItem[mCurrentItem] = buildItemFromData(0);
1759 if (mViewItem[mCurrentItem] == null) {
1762 mViewItem[mCurrentItem].setLeftPosition(0);
1763 for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) {
1764 mViewItem[i] = buildItemFromData(mViewItem[i - 1].getId() + 1);
1765 if (mViewItem[i] == null) {
1770 // Ensure that the views in mViewItem will layout the first in the
1771 // center of the display upon a reload.
1773 mScale = FILM_STRIP_SCALE;
1775 adjustChildZOrder();
1778 if (mListener != null) {
1779 mListener.onDataReloaded();
1780 mListener.onDataFocusChanged(prevId, mViewItem[mCurrentItem].getId());
1784 private void promoteData(int itemID, int dataID) {
1785 if (mListener != null) {
1786 mListener.onFocusedDataPromoted(dataID);
1790 private void demoteData(int itemID, int dataID) {
1791 if (mListener != null) {
1792 mListener.onFocusedDataDemoted(dataID);
1796 private void onEnterFilmstrip() {
1797 if (mListener != null) {
1798 mListener.onEnterFilmstrip(getCurrentId());
1802 private void onLeaveFilmstrip() {
1803 if (mListener != null) {
1804 mListener.onLeaveFilmstrip(getCurrentId());
1808 private void onEnterFullScreen() {
1809 mFullScreenUIHidden = false;
1810 if (mListener != null) {
1811 mListener.onEnterFullScreenUiShown(getCurrentId());
1815 private void onLeaveFullScreen() {
1816 if (mListener != null) {
1817 mListener.onLeaveFullScreenUiShown(getCurrentId());
1821 private void onEnterFullScreenUiHidden() {
1822 mFullScreenUIHidden = true;
1823 if (mListener != null) {
1824 mListener.onEnterFullScreenUiHidden(getCurrentId());
1828 private void onLeaveFullScreenUiHidden() {
1829 mFullScreenUIHidden = false;
1830 if (mListener != null) {
1831 mListener.onLeaveFullScreenUiHidden(getCurrentId());
1835 private void onEnterZoomView() {
1836 if (mListener != null) {
1837 mListener.onEnterZoomView(getCurrentId());
1841 private void onLeaveZoomView() {
1842 mController.setSurroundingViewsVisible(true);
1846 * MyController controls all the geometry animations. It passively tells the
1847 * geometry information on demand.
1849 private class MyController implements FilmstripController {
1851 private final ValueAnimator mScaleAnimator;
1852 private ValueAnimator mZoomAnimator;
1853 private AnimatorSet mFlingAnimator;
1855 private final MyScroller mScroller;
1856 private boolean mCanStopScroll;
1858 private final MyScroller.Listener mScrollerListener =
1859 new MyScroller.Listener() {
1861 public void onScrollUpdate(int currX, int currY) {
1864 boolean stopScroll = clampCenterX();
1866 mController.stopScrolling(true);
1872 public void onScrollEnd() {
1873 mCanStopScroll = true;
1874 if (mViewItem[mCurrentItem] == null) {
1878 if (isCurrentItemCentered()
1879 && isViewTypeSticky(mViewItem[mCurrentItem])) {
1880 // Special case for the scrolling end on the camera
1887 private final ValueAnimator.AnimatorUpdateListener mScaleAnimatorUpdateListener =
1888 new ValueAnimator.AnimatorUpdateListener() {
1890 public void onAnimationUpdate(ValueAnimator animation) {
1891 if (mViewItem[mCurrentItem] == null) {
1894 mScale = (Float) animation.getAnimatedValue();
1899 MyController(Context context) {
1900 TimeInterpolator decelerateInterpolator = new DecelerateInterpolator(1.5f);
1901 mScroller = new MyScroller(mActivity,
1902 new Handler(mActivity.getMainLooper()),
1903 mScrollerListener, decelerateInterpolator);
1904 mCanStopScroll = true;
1906 mScaleAnimator = new ValueAnimator();
1907 mScaleAnimator.addUpdateListener(mScaleAnimatorUpdateListener);
1908 mScaleAnimator.setInterpolator(decelerateInterpolator);
1909 mScaleAnimator.addListener(new Animator.AnimatorListener() {
1911 public void onAnimationStart(Animator animator) {
1912 if (mScale == FULL_SCREEN_SCALE) {
1913 onLeaveFullScreen();
1915 if (mScale == FILM_STRIP_SCALE) {
1922 public void onAnimationEnd(Animator animator) {
1923 if (mScale == FULL_SCREEN_SCALE) {
1924 onEnterFullScreen();
1926 if (mScale == FILM_STRIP_SCALE) {
1930 zoomAtIndexChanged();
1934 public void onAnimationCancel(Animator animator) {
1939 public void onAnimationRepeat(Animator animator) {
1946 public void setImageGap(int imageGap) {
1947 FilmstripView.this.setViewGap(imageGap);
1951 public int getCurrentId() {
1952 return FilmstripView.this.getCurrentId();
1956 public void setDataAdapter(DataAdapter adapter) {
1957 FilmstripView.this.setDataAdapter(adapter);
1961 public boolean inFilmstrip() {
1962 return FilmstripView.this.inFilmstrip();
1966 public boolean inFullScreen() {
1967 return FilmstripView.this.inFullScreen();
1971 public boolean isCameraPreview() {
1972 return FilmstripView.this.isCameraPreview();
1976 public boolean inCameraFullscreen() {
1977 return FilmstripView.this.inCameraFullscreen();
1981 public void setListener(FilmstripListener l) {
1982 FilmstripView.this.setListener(l);
1986 public boolean isScrolling() {
1987 return !mScroller.isFinished();
1991 public boolean isScaling() {
1992 return mScaleAnimator.isRunning();
1995 private int estimateMinX(int dataID, int leftPos, int viewWidth) {
1996 return leftPos - (dataID + 100) * (viewWidth + mViewGapInPixel);
1999 private int estimateMaxX(int dataID, int leftPos, int viewWidth) {
2001 + (mDataAdapter.getTotalNumber() - dataID + 100)
2002 * (viewWidth + mViewGapInPixel);
2005 /** Zoom all the way in or out on the image at the given pivot point. */
2006 private void zoomAt(final ViewItem current, final float focusX, final float focusY) {
2007 // End previous zoom animation, if any
2008 if (mZoomAnimator != null) {
2009 mZoomAnimator.end();
2011 // Calculate end scale
2012 final float maxScale = getCurrentDataMaxScale(false);
2013 final float endScale = mScale < maxScale - maxScale * TOLERANCE
2014 ? maxScale : FULL_SCREEN_SCALE;
2016 mZoomAnimator = new ValueAnimator();
2017 mZoomAnimator.setFloatValues(mScale, endScale);
2018 mZoomAnimator.setDuration(ZOOM_ANIMATION_DURATION_MS);
2019 mZoomAnimator.addListener(new Animator.AnimatorListener() {
2021 public void onAnimationStart(Animator animation) {
2022 if (mScale == FULL_SCREEN_SCALE) {
2023 if (mFullScreenUIHidden) {
2024 onLeaveFullScreenUiHidden();
2026 onLeaveFullScreen();
2028 setSurroundingViewsVisible(false);
2029 } else if (inZoomView()) {
2032 cancelLoadingZoomedImage();
2036 public void onAnimationEnd(Animator animation) {
2037 // Make sure animation ends up having the correct scale even
2038 // if it is cancelled before it finishes
2039 if (mScale != endScale) {
2040 current.postScale(focusX, focusY, endScale / mScale, mDrawArea.width(),
2041 mDrawArea.height());
2045 if (inFullScreen()) {
2046 setSurroundingViewsVisible(true);
2047 mZoomView.setVisibility(GONE);
2048 current.resetTransform();
2049 onEnterFullScreenUiHidden();
2051 mController.loadZoomedImage();
2054 mZoomAnimator = null;
2055 zoomAtIndexChanged();
2059 public void onAnimationCancel(Animator animation) {
2064 public void onAnimationRepeat(Animator animation) {
2069 mZoomAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2071 public void onAnimationUpdate(ValueAnimator animation) {
2072 float newScale = (Float) animation.getAnimatedValue();
2073 float postScale = newScale / mScale;
2075 current.postScale(focusX, focusY, postScale, mDrawArea.width(),
2076 mDrawArea.height());
2079 mZoomAnimator.start();
2083 public void scroll(float deltaX) {
2084 if (!stopScrolling(false)) {
2089 boolean stopScroll = clampCenterX();
2091 mController.stopScrolling(true);
2097 public void fling(float velocityX) {
2098 if (!stopScrolling(false)) {
2101 final ViewItem item = mViewItem[mCurrentItem];
2106 float scaledVelocityX = velocityX / mScale;
2107 if (inFullScreen() && isViewTypeSticky(item) && scaledVelocityX < 0) {
2108 // Swipe left in camera preview.
2113 // Estimation of possible length on the left. To ensure the
2114 // velocity doesn't become too slow eventually, we add a huge number
2115 // to the estimated maximum.
2116 int minX = estimateMinX(item.getId(), item.getLeftPosition(), w);
2117 // Estimation of possible length on the right. Likewise, exaggerate
2118 // the possible maximum too.
2119 int maxX = estimateMaxX(item.getId(), item.getLeftPosition(), w);
2120 mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0);
2123 void flingInsideZoomView(float velocityX, float velocityY) {
2124 if (!inZoomView()) {
2128 final ViewItem current = mViewItem[mCurrentItem];
2129 if (current == null) {
2133 final int factor = DECELERATION_FACTOR;
2134 // Deceleration curve for distance:
2135 // S(t) = s + (e - s) * (1 - (1 - t/T) ^ factor)
2136 // Need to find the ending distance (e), so that the starting
2137 // velocity is the velocity of fling.
2138 // Velocity is the derivative of distance
2139 // V(t) = (e - s) * factor * (-1) * (1 - t/T) ^ (factor - 1) * (-1/T)
2140 // = (e - s) * factor * (1 - t/T) ^ (factor - 1) / T
2141 // Since V(0) = V0, we have e = T / factor * V0 + s
2143 // Duration T should be long enough so that at the end of the fling,
2144 // image moves at 1 pixel/s for about P = 50ms = 0.05s
2145 // i.e. V(T - P) = 1
2146 // V(T - P) = V0 * (1 - (T -P) /T) ^ (factor - 1) = 1
2147 // T = P * V0 ^ (1 / (factor -1))
2149 final float velocity = Math.max(Math.abs(velocityX), Math.abs(velocityY));
2150 // Dynamically calculate duration
2151 final float duration = (float) (FLING_COASTING_DURATION_S
2152 * Math.pow(velocity, (1f / (factor - 1f))));
2154 final float translationX = current.getTranslationX() * mScale;
2155 final float translationY = current.getTranslationY() * mScale;
2157 final ValueAnimator decelerationX = ValueAnimator.ofFloat(translationX,
2158 translationX + duration / factor * velocityX);
2159 final ValueAnimator decelerationY = ValueAnimator.ofFloat(translationY,
2160 translationY + duration / factor * velocityY);
2162 decelerationY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2164 public void onAnimationUpdate(ValueAnimator animation) {
2165 float transX = (Float) decelerationX.getAnimatedValue();
2166 float transY = (Float) decelerationY.getAnimatedValue();
2168 current.updateTransform(transX, transY, mScale,
2169 mScale, mDrawArea.width(), mDrawArea.height());
2173 mFlingAnimator = new AnimatorSet();
2174 mFlingAnimator.play(decelerationX).with(decelerationY);
2175 mFlingAnimator.setDuration((int) (duration * 1000));
2176 mFlingAnimator.setInterpolator(new TimeInterpolator() {
2178 public float getInterpolation(float input) {
2179 return (float) (1.0f - Math.pow((1.0f - input), factor));
2182 mFlingAnimator.addListener(new Animator.AnimatorListener() {
2183 private boolean mCancelled = false;
2186 public void onAnimationStart(Animator animation) {
2191 public void onAnimationEnd(Animator animation) {
2195 mFlingAnimator = null;
2199 public void onAnimationCancel(Animator animation) {
2204 public void onAnimationRepeat(Animator animation) {
2208 mFlingAnimator.start();
2212 public boolean stopScrolling(boolean forced) {
2213 if (!isScrolling()) {
2215 } else if (!mCanStopScroll && !forced) {
2218 mScroller.forceFinished(true);
2222 private void stopScale() {
2223 mScaleAnimator.cancel();
2227 public void scrollToPosition(int position, int duration, boolean interruptible) {
2228 if (mViewItem[mCurrentItem] == null) {
2231 mCanStopScroll = interruptible;
2232 mScroller.startScroll(mCenterX, 0, position - mCenterX, 0, duration);
2236 public boolean goToNextItem() {
2237 return goToItem(mCurrentItem + 1);
2241 public boolean goToPreviousItem() {
2242 return goToItem(mCurrentItem - 1);
2245 private boolean goToItem(int itemIndex) {
2246 final ViewItem nextItem = mViewItem[itemIndex];
2247 if (nextItem == null) {
2250 stopScrolling(true);
2251 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS * 2, false);
2253 if (isViewTypeSticky(mViewItem[mCurrentItem])) {
2254 // Special case when moving from camera preview.
2255 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
2260 private void scaleTo(float scale, int duration) {
2261 if (mViewItem[mCurrentItem] == null) {
2265 mScaleAnimator.setDuration(duration);
2266 mScaleAnimator.setFloatValues(mScale, scale);
2267 mScaleAnimator.start();
2271 public void goToFilmstrip() {
2272 if (mViewItem[mCurrentItem] == null) {
2275 if (mScale == FILM_STRIP_SCALE) {
2278 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
2280 final ViewItem currItem = mViewItem[mCurrentItem];
2281 final ViewItem nextItem = mViewItem[mCurrentItem + 1];
2282 if (currItem.getId() == 0 && isViewTypeSticky(currItem) && nextItem != null) {
2283 // Deal with the special case of swiping in camera preview.
2284 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, false);
2287 if (mScale == FILM_STRIP_SCALE) {
2293 public void goToFullScreen() {
2294 if (inFullScreen()) {
2298 scaleTo(FULL_SCREEN_SCALE, GEOMETRY_ADJUST_TIME_MS);
2301 private void cancelFlingAnimation() {
2302 // Cancels flinging for zoomed images
2303 if (isFlingAnimationRunning()) {
2304 mFlingAnimator.cancel();
2308 private void cancelZoomAnimation() {
2309 if (isZoomAnimationRunning()) {
2310 mZoomAnimator.cancel();
2314 private void setSurroundingViewsVisible(boolean visible) {
2315 // Hide everything on the left
2316 // TODO: Need to find a better way to toggle the visibility of views
2317 // around the current view.
2318 for (int i = 0; i < mCurrentItem; i++) {
2319 if (i == mCurrentItem || mViewItem[i] == null) {
2322 mViewItem[i].setVisibility(visible ? VISIBLE : INVISIBLE);
2326 private Uri getCurrentUri() {
2327 ViewItem curr = mViewItem[mCurrentItem];
2331 return mDataAdapter.getImageData(curr.getId()).getUri();
2335 * Here we only support up to 1:1 image zoom (i.e. a 100% view of the
2336 * actual pixels). The max scale that we can apply on the view should
2337 * make the view same size as the image, in pixels.
2339 private float getCurrentDataMaxScale(boolean allowOverScale) {
2340 ViewItem curr = mViewItem[mCurrentItem];
2342 return FULL_SCREEN_SCALE;
2344 ImageData imageData = mDataAdapter.getImageData(curr.getId());
2345 if (imageData == null || !imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) {
2346 return FULL_SCREEN_SCALE;
2348 float imageWidth = imageData.getWidth();
2349 if (imageData.getRotation() == 90
2350 || imageData.getRotation() == 270) {
2351 imageWidth = imageData.getHeight();
2353 float scale = imageWidth / curr.getWidth();
2354 if (allowOverScale) {
2355 // In addition to the scale we apply to the view for 100% view
2356 // (i.e. each pixel on screen corresponds to a pixel in image)
2357 // we allow scaling beyond that for better detail viewing.
2358 scale *= mOverScaleFactor;
2363 private void loadZoomedImage() {
2364 if (!inZoomView()) {
2367 ViewItem curr = mViewItem[mCurrentItem];
2371 ImageData imageData = mDataAdapter.getImageData(curr.getId());
2372 if (!imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) {
2375 Uri uri = getCurrentUri();
2376 RectF viewRect = curr.getViewRect();
2377 if (uri == null || uri == Uri.EMPTY) {
2380 int orientation = imageData.getRotation();
2381 mZoomView.loadBitmap(uri, orientation, viewRect);
2384 private void cancelLoadingZoomedImage() {
2385 mZoomView.cancelPartialDecodingTask();
2389 public void goToFirstItem() {
2390 if (mViewItem[mCurrentItem] == null) {
2394 // TODO: animate to camera if it is still in the mViewItem buffer
2395 // versus a full reload which will perform an immediate transition
2399 public boolean inZoomView() {
2400 return FilmstripView.this.inZoomView();
2403 public boolean isFlingAnimationRunning() {
2404 return mFlingAnimator != null && mFlingAnimator.isRunning();
2407 public boolean isZoomAnimationRunning() {
2408 return mZoomAnimator != null && mZoomAnimator.isRunning();
2412 private boolean isCurrentItemCentered() {
2413 return mViewItem[mCurrentItem].getCenterX() == mCenterX;
2416 private static class MyScroller {
2417 public interface Listener {
2418 public void onScrollUpdate(int currX, int currY);
2420 public void onScrollEnd();
2423 private final Handler mHandler;
2424 private final Listener mListener;
2426 private final Scroller mScroller;
2428 private final ValueAnimator mXScrollAnimator;
2429 private final Runnable mScrollChecker = new Runnable() {
2432 boolean newPosition = mScroller.computeScrollOffset();
2434 mListener.onScrollEnd();
2437 mListener.onScrollUpdate(mScroller.getCurrX(), mScroller.getCurrY());
2438 mHandler.removeCallbacks(this);
2439 mHandler.post(this);
2443 private final ValueAnimator.AnimatorUpdateListener mXScrollAnimatorUpdateListener =
2444 new ValueAnimator.AnimatorUpdateListener() {
2446 public void onAnimationUpdate(ValueAnimator animation) {
2447 mListener.onScrollUpdate((Integer) animation.getAnimatedValue(), 0);
2451 private final Animator.AnimatorListener mXScrollAnimatorListener =
2452 new Animator.AnimatorListener() {
2454 public void onAnimationCancel(Animator animation) {
2459 public void onAnimationEnd(Animator animation) {
2460 mListener.onScrollEnd();
2464 public void onAnimationRepeat(Animator animation) {
2469 public void onAnimationStart(Animator animation) {
2474 public MyScroller(Context ctx, Handler handler, Listener listener,
2475 TimeInterpolator interpolator) {
2477 mListener = listener;
2478 mScroller = new Scroller(ctx);
2479 mXScrollAnimator = new ValueAnimator();
2480 mXScrollAnimator.addUpdateListener(mXScrollAnimatorUpdateListener);
2481 mXScrollAnimator.addListener(mXScrollAnimatorListener);
2482 mXScrollAnimator.setInterpolator(interpolator);
2486 int startX, int startY,
2487 int velocityX, int velocityY,
2489 int minY, int maxY) {
2490 mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
2494 public void startScroll(int startX, int startY, int dx, int dy) {
2495 mScroller.startScroll(startX, startY, dx, dy);
2499 /** Only starts and updates scroll in x-axis. */
2500 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
2501 mXScrollAnimator.cancel();
2502 mXScrollAnimator.setDuration(duration);
2503 mXScrollAnimator.setIntValues(startX, startX + dx);
2504 mXScrollAnimator.start();
2507 public boolean isFinished() {
2508 return (mScroller.isFinished() && !mXScrollAnimator.isRunning());
2511 public void forceFinished(boolean finished) {
2512 mScroller.forceFinished(finished);
2514 mXScrollAnimator.cancel();
2518 private void runChecker() {
2519 if (mHandler == null || mListener == null) {
2522 mHandler.removeCallbacks(mScrollChecker);
2523 mHandler.post(mScrollChecker);
2527 private class MyGestureReceiver implements FilmstripGestureRecognizer.Listener {
2529 private static final int SCROLL_DIR_NONE = 0;
2530 private static final int SCROLL_DIR_VERTICAL = 1;
2531 private static final int SCROLL_DIR_HORIZONTAL = 2;
2532 // Indicating the current trend of scaling is up (>1) or down (<1).
2533 private float mScaleTrend;
2534 private float mMaxScale;
2535 private int mScrollingDirection = SCROLL_DIR_NONE;
2536 private long mLastDownTime;
2537 private float mLastDownY;
2540 public boolean onSingleTapUp(float x, float y) {
2541 ViewItem centerItem = mViewItem[mCurrentItem];
2542 if (inFilmstrip()) {
2543 if (centerItem != null && centerItem.areaContains(x, y)) {
2544 mController.goToFullScreen();
2547 } else if (inFullScreen()) {
2548 if (mFullScreenUIHidden) {
2549 onLeaveFullScreenUiHidden();
2550 onEnterFullScreen();
2552 onLeaveFullScreen();
2553 onEnterFullScreenUiHidden();
2561 public boolean onDoubleTap(float x, float y) {
2562 ViewItem current = mViewItem[mCurrentItem];
2563 if (current == null) {
2566 if (inFilmstrip()) {
2567 mController.goToFullScreen();
2569 } else if (mScale < FULL_SCREEN_SCALE || inCameraFullscreen()) {
2572 if (!mController.stopScrolling(false)) {
2575 if (inFullScreen()) {
2576 mController.zoomAt(current, x, y);
2577 checkItemAtMaxSize();
2579 } else if (mScale > FULL_SCREEN_SCALE) {
2581 mController.zoomAt(current, x, y);
2587 public boolean onDown(float x, float y) {
2588 mLastDownTime = SystemClock.uptimeMillis();
2590 mController.cancelFlingAnimation();
2591 if (!mController.stopScrolling(false)) {
2599 public boolean onUp(float x, float y) {
2600 ViewItem currItem = mViewItem[mCurrentItem];
2601 if (currItem == null) {
2604 if (mController.isZoomAnimationRunning() || mController.isFlingAnimationRunning()) {
2608 mController.loadZoomedImage();
2611 float promoteHeight = getHeight() * PROMOTE_HEIGHT_RATIO;
2612 float velocityPromoteHeight = getHeight() * VELOCITY_PROMOTE_HEIGHT_RATIO;
2613 mIsUserScrolling = false;
2614 mScrollingDirection = SCROLL_DIR_NONE;
2615 // Finds items promoted/demoted.
2616 float speedY = Math.abs(y - mLastDownY)
2617 / (SystemClock.uptimeMillis() - mLastDownTime);
2618 for (int i = 0; i < BUFFER_SIZE; i++) {
2619 if (mViewItem[i] == null) {
2622 float transY = mViewItem[i].getTranslationY();
2626 int id = mViewItem[i].getId();
2628 if (mDataAdapter.getImageData(id)
2629 .isUIActionSupported(ImageData.ACTION_DEMOTE)
2630 && ((transY > promoteHeight)
2631 || (transY > velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) {
2633 } else if (mDataAdapter.getImageData(id)
2634 .isUIActionSupported(ImageData.ACTION_PROMOTE)
2635 && (transY < -promoteHeight
2636 || (transY < -velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) {
2639 // put the view back.
2640 slideViewBack(mViewItem[i]);
2644 // The data might be changed. Re-check.
2645 currItem = mViewItem[mCurrentItem];
2646 if (currItem == null) {
2650 int currId = currItem.getId();
2651 if (mCenterX > currItem.getCenterX() + CAMERA_PREVIEW_SWIPE_THRESHOLD && currId == 0 &&
2652 isViewTypeSticky(currItem) && mDataIdOnUserScrolling == 0) {
2653 mController.goToFilmstrip();
2654 // Special case to go from camera preview to the next photo.
2655 if (mViewItem[mCurrentItem + 1] != null) {
2656 mController.scrollToPosition(
2657 mViewItem[mCurrentItem + 1].getCenterX(),
2658 GEOMETRY_ADJUST_TIME_MS, false);
2664 if (isCurrentItemCentered() && currId == 0 && isViewTypeSticky(currItem)) {
2665 mController.goToFullScreen();
2667 if (mDataIdOnUserScrolling == 0 && currId != 0) {
2668 // Special case to go to filmstrip when the user scroll away
2669 // from the camera preview and the current one is not the
2671 mController.goToFilmstrip();
2672 mDataIdOnUserScrolling = currId;
2680 public void onLongPress(float x, float y) {
2681 final int dataId = getCurrentId();
2685 mListener.onFocusedDataLongPressed(dataId);
2689 public boolean onScroll(float x, float y, float dx, float dy) {
2690 final ViewItem currItem = mViewItem[mCurrentItem];
2691 if (currItem == null) {
2694 if (inFullScreen() && !mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
2698 // When image is zoomed in to be bigger than the screen
2700 ViewItem curr = mViewItem[mCurrentItem];
2701 float transX = curr.getTranslationX() * mScale - dx;
2702 float transY = curr.getTranslationY() * mScale - dy;
2703 curr.updateTransform(transX, transY, mScale, mScale, mDrawArea.width(),
2704 mDrawArea.height());
2707 int deltaX = (int) (dx / mScale);
2708 // Forces the current scrolling to stop.
2709 mController.stopScrolling(true);
2710 if (!mIsUserScrolling) {
2711 mIsUserScrolling = true;
2712 mDataIdOnUserScrolling = mViewItem[mCurrentItem].getId();
2714 if (inFilmstrip()) {
2715 // Disambiguate horizontal/vertical first.
2716 if (mScrollingDirection == SCROLL_DIR_NONE) {
2717 mScrollingDirection = (Math.abs(dx) > Math.abs(dy)) ? SCROLL_DIR_HORIZONTAL :
2718 SCROLL_DIR_VERTICAL;
2720 if (mScrollingDirection == SCROLL_DIR_HORIZONTAL) {
2721 if (mCenterX == currItem.getCenterX() && currItem.getId() == 0 && dx < 0) {
2722 // Already at the beginning, don't process the swipe.
2723 mIsUserScrolling = false;
2724 mScrollingDirection = SCROLL_DIR_NONE;
2727 mController.scroll(deltaX);
2729 // Vertical part. Promote or demote.
2731 Rect hitRect = new Rect();
2732 for (; hit < BUFFER_SIZE; hit++) {
2733 if (mViewItem[hit] == null) {
2736 mViewItem[hit].getHitRect(hitRect);
2737 if (hitRect.contains((int) x, (int) y)) {
2741 if (hit == BUFFER_SIZE) {
2746 ImageData data = mDataAdapter.getImageData(mViewItem[hit].getId());
2747 float transY = mViewItem[hit].getTranslationY() - dy / mScale;
2748 if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) &&
2752 if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) &&
2756 mViewItem[hit].setTranslationY(transY);
2758 } else if (inFullScreen()) {
2759 if (mViewItem[mCurrentItem] == null || (deltaX < 0 && mCenterX <=
2760 currItem.getCenterX() && currItem.getId() == 0)) {
2763 // Multiplied by 1.2 to make it more easy to swipe.
2764 mController.scroll((int) (deltaX * 1.2));
2772 public boolean onFling(float velocityX, float velocityY) {
2773 final ViewItem currItem = mViewItem[mCurrentItem];
2774 if (currItem == null) {
2777 if (!mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
2781 // Fling within the zoomed image
2782 mController.flingInsideZoomView(velocityX, velocityY);
2785 if (Math.abs(velocityX) < Math.abs(velocityY)) {
2786 // ignore vertical fling.
2790 // In full-screen, fling of a velocity above a threshold should go
2791 // to the next/prev photos
2792 if (mScale == FULL_SCREEN_SCALE) {
2793 int currItemCenterX = currItem.getCenterX();
2795 if (velocityX > 0) { // left
2796 if (mCenterX > currItemCenterX) {
2797 // The visually previous item is actually the current
2799 mController.scrollToPosition(
2800 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
2803 ViewItem prevItem = mViewItem[mCurrentItem - 1];
2804 if (prevItem == null) {
2807 mController.scrollToPosition(
2808 prevItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true);
2810 if (mController.stopScrolling(false)) {
2811 if (mCenterX < currItemCenterX) {
2812 // The visually next item is actually the current
2814 mController.scrollToPosition(
2815 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
2818 final ViewItem nextItem = mViewItem[mCurrentItem + 1];
2819 if (nextItem == null) {
2822 mController.scrollToPosition(
2823 nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true);
2824 if (isViewTypeSticky(currItem)) {
2825 mController.goToFilmstrip();
2831 if (mScale == FILM_STRIP_SCALE) {
2832 mController.fling(velocityX);
2838 public boolean onScaleBegin(float focusX, float focusY) {
2839 if (inCameraFullscreen()) {
2845 // If the image is smaller than screen size, we should allow to zoom
2846 // in to full screen size
2847 mMaxScale = Math.max(mController.getCurrentDataMaxScale(true), FULL_SCREEN_SCALE);
2852 public boolean onScale(float focusX, float focusY, float scale) {
2853 if (inCameraFullscreen()) {
2857 mScaleTrend = mScaleTrend * 0.3f + scale * 0.7f;
2858 float newScale = mScale * scale;
2859 if (mScale < FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) {
2860 if (newScale <= FILM_STRIP_SCALE) {
2861 newScale = FILM_STRIP_SCALE;
2863 // Scaled view is smaller than or equal to screen size both
2864 // before and after scaling
2865 if (mScale != newScale) {
2866 if (mScale == FILM_STRIP_SCALE) {
2869 if (newScale == FILM_STRIP_SCALE) {
2875 } else if (mScale < FULL_SCREEN_SCALE && newScale >= FULL_SCREEN_SCALE) {
2876 // Going from smaller than screen size to bigger than or equal
2878 if (mScale == FILM_STRIP_SCALE) {
2881 mScale = FULL_SCREEN_SCALE;
2882 onEnterFullScreen();
2883 mController.setSurroundingViewsVisible(false);
2885 } else if (mScale >= FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) {
2886 // Going from bigger than or equal to screen size to smaller
2888 if (inFullScreen()) {
2889 if (mFullScreenUIHidden) {
2890 onLeaveFullScreenUiHidden();
2892 onLeaveFullScreen();
2901 // Scaled view bigger than or equal to screen size both before
2902 // and after scaling
2903 if (!inZoomView()) {
2904 mController.setSurroundingViewsVisible(false);
2906 ViewItem curr = mViewItem[mCurrentItem];
2907 // Make sure the image is not overly scaled
2908 newScale = Math.min(newScale, mMaxScale);
2909 if (newScale == mScale) {
2912 float postScale = newScale / mScale;
2913 curr.postScale(focusX, focusY, postScale, mDrawArea.width(), mDrawArea.height());
2915 if (mScale == FULL_SCREEN_SCALE) {
2916 onEnterFullScreen();
2920 checkItemAtMaxSize();
2926 public void onScaleEnd() {
2927 zoomAtIndexChanged();
2928 if (mScale > FULL_SCREEN_SCALE + TOLERANCE) {
2931 mController.setSurroundingViewsVisible(true);
2932 if (mScale <= FILM_STRIP_SCALE + TOLERANCE) {
2933 mController.goToFilmstrip();
2934 } else if (mScaleTrend > 1f || mScale > FULL_SCREEN_SCALE - TOLERANCE) {
2936 mScale = FULL_SCREEN_SCALE;
2939 mController.goToFullScreen();
2941 mController.goToFilmstrip();