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.KeyEvent;
38 import android.view.MotionEvent;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.accessibility.AccessibilityNodeInfo;
42 import android.view.animation.DecelerateInterpolator;
43 import android.widget.Scroller;
45 import com.android.camera.CameraActivity;
46 import com.android.camera.debug.Log;
47 import com.android.camera.filmstrip.DataAdapter;
48 import com.android.camera.filmstrip.FilmstripController;
49 import com.android.camera.filmstrip.ImageData;
50 import com.android.camera.ui.FilmstripGestureRecognizer;
51 import com.android.camera.ui.ZoomView;
52 import com.android.camera.util.ApiHelper;
53 import com.android.camera.util.CameraUtil;
54 import com.android.camera2.R;
56 import java.util.ArrayDeque;
57 import java.util.Arrays;
58 import java.util.Queue;
60 public class FilmstripView extends ViewGroup {
61 private static final Log.Tag TAG = new Log.Tag("FilmstripView");
63 private static final int BUFFER_SIZE = 5;
64 private static final int GEOMETRY_ADJUST_TIME_MS = 400;
65 private static final int SNAP_IN_CENTER_TIME_MS = 600;
66 private static final float FLING_COASTING_DURATION_S = 0.05f;
67 private static final int ZOOM_ANIMATION_DURATION_MS = 200;
68 private static final int CAMERA_PREVIEW_SWIPE_THRESHOLD = 300;
69 private static final float FILM_STRIP_SCALE = 0.7f;
70 private static final float FULL_SCREEN_SCALE = 1f;
72 // The min velocity at which the user must have moved their finger in
73 // pixels per millisecond to count a vertical gesture as a promote/demote
74 // at short vertical distances.
75 private static final float PROMOTE_VELOCITY = 3.5f;
76 // The min distance relative to this view's height the user must have
77 // moved their finger to count a vertical gesture as a promote/demote if
78 // they moved their finger at least at PROMOTE_VELOCITY.
79 private static final float VELOCITY_PROMOTE_HEIGHT_RATIO = 1/10f;
80 // The min distance relative to this view's height the user must have
81 // moved their finger to count a vertical gesture as a promote/demote if
82 // they moved their finger at less than PROMOTE_VELOCITY.
83 private static final float PROMOTE_HEIGHT_RATIO = 1/2f;
85 private static final float TOLERANCE = 0.1f;
86 // Only check for intercepting touch events within first 500ms
87 private static final int SWIPE_TIME_OUT = 500;
88 private static final int DECELERATION_FACTOR = 4;
89 private static final float MOUSE_SCROLL_FACTOR = 128f;
91 private CameraActivity mActivity;
92 private FilmstripGestureRecognizer mGestureRecognizer;
93 private FilmstripGestureRecognizer.Listener mGestureListener;
94 private DataAdapter mDataAdapter;
95 private int mViewGapInPixel;
96 private final Rect mDrawArea = new Rect();
98 private final int mCurrentItem = (BUFFER_SIZE - 1) / 2;
100 private MyController mController;
101 private int mCenterX = -1;
102 private final ViewItem[] mViewItem = new ViewItem[BUFFER_SIZE];
104 private FilmstripController.FilmstripListener mListener;
105 private ZoomView mZoomView = null;
107 private MotionEvent mDown;
108 private boolean mCheckToIntercept = true;
110 private TimeInterpolator mViewAnimInterpolator;
112 // This is true if and only if the user is scrolling,
113 private boolean mIsUserScrolling;
114 private int mDataIdOnUserScrolling;
115 private float mOverScaleFactor = 1f;
117 private boolean mFullScreenUIHidden = false;
118 private final SparseArray<Queue<View>> recycledViews = new SparseArray<Queue<View>>();
121 * A helper class to tract and calculate the view coordination.
123 private class ViewItem {
125 /** The position of the left of the view in the whole filmstrip. */
126 private int mLeftPosition;
127 private final View mView;
128 private final ImageData mData;
129 private final RectF mViewArea;
130 private boolean mMaximumBitmapRequested;
132 private ValueAnimator mTranslationXAnimator;
133 private ValueAnimator mTranslationYAnimator;
134 private ValueAnimator mAlphaAnimator;
139 * @param id The id of the data from
140 * {@link com.android.camera.filmstrip.DataAdapter}.
141 * @param v The {@code View} representing the data.
143 public ViewItem(int id, View v, ImageData data) {
149 mMaximumBitmapRequested = false;
151 mViewArea = new RectF();
154 public boolean isMaximumBitmapRequested() {
155 return mMaximumBitmapRequested;
158 public void setMaximumBitmapRequested() {
159 mMaximumBitmapRequested = true;
163 * Returns the data id from
164 * {@link com.android.camera.filmstrip.DataAdapter}.
171 * Sets the data id from
172 * {@link com.android.camera.filmstrip.DataAdapter}.
174 public void setId(int id) {
178 /** Sets the left position of the view in the whole filmstrip. */
179 public void setLeftPosition(int pos) {
183 /** Returns the left position of the view in the whole filmstrip. */
184 public int getLeftPosition() {
185 return mLeftPosition;
188 /** Returns the translation of Y regarding the view scale. */
189 public float getTranslationY() {
190 return mView.getTranslationY() / mScale;
193 /** Returns the translation of X regarding the view scale. */
194 public float getTranslationX() {
195 return mView.getTranslationX() / mScale;
198 /** Sets the translation of Y regarding the view scale. */
199 public void setTranslationY(float transY) {
200 mView.setTranslationY(transY * mScale);
203 /** Sets the translation of X regarding the view scale. */
204 public void setTranslationX(float transX) {
205 mView.setTranslationX(transX * mScale);
208 /** Forwarding of {@link android.view.View#setAlpha(float)}. */
209 public void setAlpha(float alpha) {
210 mView.setAlpha(alpha);
213 /** Forwarding of {@link android.view.View#getAlpha()}. */
214 public float getAlpha() {
215 return mView.getAlpha();
218 /** Forwarding of {@link android.view.View#getMeasuredWidth()}. */
219 public int getMeasuredWidth() {
220 return mView.getMeasuredWidth();
224 * Animates the X translation of the view. Note: the animated value is
225 * not set directly by {@link android.view.View#setTranslationX(float)}
226 * because the value might be changed during in {@code onLayout()}.
227 * The animated value of X translation is specially handled in {@code
230 * @param targetX The final value.
231 * @param duration_ms The duration of the animation.
232 * @param interpolator Time interpolator.
234 public void animateTranslationX(
235 float targetX, long duration_ms, TimeInterpolator interpolator) {
236 if (mTranslationXAnimator == null) {
237 mTranslationXAnimator = new ValueAnimator();
238 mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
240 public void onAnimationUpdate(ValueAnimator valueAnimator) {
241 // We invalidate the filmstrip view instead of setting the
242 // translation X because the translation X of the view is
243 // touched in onLayout(). See the documentation of
244 // animateTranslationX().
249 runAnimation(mTranslationXAnimator, getTranslationX(), targetX, duration_ms,
254 * Animates the Y translation of the view.
256 * @param targetY The final value.
257 * @param duration_ms The duration of the animation.
258 * @param interpolator Time interpolator.
260 public void animateTranslationY(
261 float targetY, long duration_ms, TimeInterpolator interpolator) {
262 if (mTranslationYAnimator == null) {
263 mTranslationYAnimator = new ValueAnimator();
264 mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
266 public void onAnimationUpdate(ValueAnimator valueAnimator) {
267 setTranslationY((Float) valueAnimator.getAnimatedValue());
271 runAnimation(mTranslationYAnimator, getTranslationY(), targetY, duration_ms,
276 * Animates the alpha value of the view.
278 * @param targetAlpha The final value.
279 * @param duration_ms The duration of the animation.
280 * @param interpolator Time interpolator.
282 public void animateAlpha(float targetAlpha, long duration_ms,
283 TimeInterpolator interpolator) {
284 if (mAlphaAnimator == null) {
285 mAlphaAnimator = new ValueAnimator();
286 mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
288 public void onAnimationUpdate(ValueAnimator valueAnimator) {
289 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue());
293 runAnimation(mAlphaAnimator, getAlpha(), targetAlpha, duration_ms, interpolator);
296 private void runAnimation(final ValueAnimator animator, final float startValue,
297 final float targetValue, final long duration_ms,
298 final TimeInterpolator interpolator) {
299 if (startValue == targetValue) {
302 animator.setInterpolator(interpolator);
303 animator.setDuration(duration_ms);
304 animator.setFloatValues(startValue, targetValue);
308 /** Adjusts the translation of X regarding the view scale. */
309 public void translateXScaledBy(float transX) {
310 setTranslationX(getTranslationX() + transX * mScale);
314 * Forwarding of {@link android.view.View#getHitRect(android.graphics.Rect)}.
316 public void getHitRect(Rect rect) {
317 mView.getHitRect(rect);
320 public int getCenterX() {
321 return mLeftPosition + mView.getMeasuredWidth() / 2;
324 /** Forwarding of {@link android.view.View#getVisibility()}. */
325 public int getVisibility() {
326 return mView.getVisibility();
329 /** Forwarding of {@link android.view.View#setVisibility(int)}. */
330 public void setVisibility(int visibility) {
331 mView.setVisibility(visibility);
335 * Notifies the {@link com.android.camera.filmstrip.DataAdapter} to
338 public void resizeView(Context context, int w, int h) {
339 mDataAdapter.resizeView(context, mDataId, mView, w, h);
343 * Adds the view of the data to the view hierarchy if necessary.
345 public void addViewToHierarchy() {
346 if (indexOfChild(mView) < 0) {
351 setVisibility(View.VISIBLE);
358 * Removes from the hierarchy. Keeps the view in the view hierarchy if
359 * view type is {@code VIEW_TYPE_STICKY} and set to invisible instead.
361 * @param force {@code true} to remove the view from the hierarchy
362 * regardless of the view type.
364 public void removeViewFromHierarchy(boolean force) {
365 if (force || mData.getViewType() != ImageData.VIEW_TYPE_STICKY) {
367 mData.recycle(mView);
368 recycleView(mView, mDataId);
370 setVisibility(View.INVISIBLE);
375 * Brings the view to front by
376 * {@link #bringChildToFront(android.view.View)}
378 public void bringViewToFront() {
379 bringChildToFront(mView);
383 * The visual x position of this view, in pixels.
385 public float getX() {
390 * The visual y position of this view, in pixels.
392 public float getY() {
397 * Forwarding of {@link android.view.View#measure(int, int)}.
399 public void measure(int widthSpec, int heightSpec) {
400 mView.measure(widthSpec, heightSpec);
403 private void layoutAt(int left, int top) {
404 mView.layout(left, top, left + mView.getMeasuredWidth(),
405 top + mView.getMeasuredHeight());
409 * The bounding rect of the view.
411 public RectF getViewRect() {
412 RectF r = new RectF();
413 r.left = mView.getX();
414 r.top = mView.getY();
415 r.right = r.left + mView.getWidth() * mView.getScaleX();
416 r.bottom = r.top + mView.getHeight() * mView.getScaleY();
420 private View getView() {
425 * Layouts the view in the area assuming the center of the area is at a
426 * specific point of the whole filmstrip.
428 * @param drawArea The area when filmstrip will show in.
429 * @param refCenter The absolute X coordination in the whole filmstrip
430 * of the center of {@code drawArea}.
431 * @param scale The scale of the view on the filmstrip.
433 public void layoutWithTranslationX(Rect drawArea, int refCenter, float scale) {
434 final float translationX =
435 ((mTranslationXAnimator != null && mTranslationXAnimator.isRunning()) ?
436 (Float) mTranslationXAnimator.getAnimatedValue() : 0);
438 (int) (drawArea.centerX() + (mLeftPosition - refCenter + translationX) * scale);
439 int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale);
441 mView.setScaleX(scale);
442 mView.setScaleY(scale);
444 // update mViewArea for touch detection.
445 int l = mView.getLeft();
446 int t = mView.getTop();
448 l + mView.getMeasuredWidth() * scale,
449 t + mView.getMeasuredHeight() * scale);
452 /** Returns true if the point is in the view. */
453 public boolean areaContains(float x, float y) {
454 return mViewArea.contains(x, y);
458 * Return the width of the view.
460 public int getWidth() {
461 return mView.getWidth();
465 * Returns the position of the left edge of the view area content is drawn in.
467 public int getDrawAreaLeft() {
468 return Math.round(mViewArea.left);
471 public void copyAttributes(ViewItem item) {
472 setLeftPosition(item.getLeftPosition());
474 setTranslationX(item.getTranslationX());
475 if (item.mTranslationXAnimator != null) {
476 mTranslationXAnimator = item.mTranslationXAnimator;
477 mTranslationXAnimator.removeAllUpdateListeners();
478 mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
480 public void onAnimationUpdate(ValueAnimator valueAnimator) {
481 // We invalidate the filmstrip view instead of setting the
482 // translation X because the translation X of the view is
483 // touched in onLayout(). See the documentation of
484 // animateTranslationX().
490 setTranslationY(item.getTranslationY());
491 if (item.mTranslationYAnimator != null) {
492 mTranslationYAnimator = item.mTranslationYAnimator;
493 mTranslationYAnimator.removeAllUpdateListeners();
494 mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
496 public void onAnimationUpdate(ValueAnimator valueAnimator) {
497 setTranslationY((Float) valueAnimator.getAnimatedValue());
502 setAlpha(item.getAlpha());
503 if (item.mAlphaAnimator != null) {
504 mAlphaAnimator = item.mAlphaAnimator;
505 mAlphaAnimator.removeAllUpdateListeners();
506 mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
508 public void onAnimationUpdate(ValueAnimator valueAnimator) {
509 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue());
516 * Apply a scale factor (i.e. {@code postScale}) on top of current scale at
517 * pivot point ({@code focusX}, {@code focusY}). Visually it should be the
518 * same as post concatenating current view's matrix with specified scale.
520 void postScale(float focusX, float focusY, float postScale, int viewportWidth,
521 int viewportHeight) {
522 float transX = mView.getTranslationX();
523 float transY = mView.getTranslationY();
524 // Pivot point is top left of the view, so we need to translate
525 // to scale around focus point
526 transX -= (focusX - getX()) * (postScale - 1f);
527 transY -= (focusY - getY()) * (postScale - 1f);
528 float scaleX = mView.getScaleX() * postScale;
529 float scaleY = mView.getScaleY() * postScale;
530 updateTransform(transX, transY, scaleX, scaleY, viewportWidth,
534 void updateTransform(float transX, float transY, float scaleX, float scaleY,
535 int viewportWidth, int viewportHeight) {
536 float left = transX + mView.getLeft();
537 float top = transY + mView.getTop();
538 RectF r = ZoomView.adjustToFitInBounds(new RectF(left, top,
539 left + mView.getWidth() * scaleX,
540 top + mView.getHeight() * scaleY),
541 viewportWidth, viewportHeight);
542 mView.setScaleX(scaleX);
543 mView.setScaleY(scaleY);
544 transX = r.left - mView.getLeft();
545 transY = r.top - mView.getTop();
546 mView.setTranslationX(transX);
547 mView.setTranslationY(transY);
550 void resetTransform() {
551 mView.setScaleX(FULL_SCREEN_SCALE);
552 mView.setScaleY(FULL_SCREEN_SCALE);
553 mView.setTranslationX(0f);
554 mView.setTranslationY(0f);
558 public String toString() {
559 return "DataID = " + mDataId + "\n\t left = " + mLeftPosition
560 + "\n\t viewArea = " + mViewArea
561 + "\n\t centerX = " + getCenterX()
562 + "\n\t view MeasuredSize = "
563 + mView.getMeasuredWidth() + ',' + mView.getMeasuredHeight()
564 + "\n\t view Size = " + mView.getWidth() + ',' + mView.getHeight()
565 + "\n\t view scale = " + mView.getScaleX();
570 public FilmstripView(Context context) {
572 init((CameraActivity) context);
576 public FilmstripView(Context context, AttributeSet attrs) {
577 super(context, attrs);
578 init((CameraActivity) context);
582 public FilmstripView(Context context, AttributeSet attrs, int defStyle) {
583 super(context, attrs, defStyle);
584 init((CameraActivity) context);
587 private void init(CameraActivity cameraActivity) {
588 setWillNotDraw(false);
589 mActivity = cameraActivity;
591 mDataIdOnUserScrolling = 0;
592 mController = new MyController(cameraActivity);
593 mViewAnimInterpolator = new DecelerateInterpolator();
594 mZoomView = new ZoomView(cameraActivity);
595 mZoomView.setVisibility(GONE);
598 mGestureListener = new MyGestureReceiver();
600 new FilmstripGestureRecognizer(cameraActivity, mGestureListener);
601 mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop);
602 DisplayMetrics metrics = new DisplayMetrics();
603 mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
604 // Allow over scaling because on high density screens, pixels are too
605 // tiny to clearly see the details at 1:1 zoom. We should not scale
606 // beyond what 1:1 would look like on a medium density screen, as
607 // scaling beyond that would only yield blur.
608 mOverScaleFactor = (float) metrics.densityDpi / (float) DisplayMetrics.DENSITY_HIGH;
609 if (mOverScaleFactor < 1f) {
610 mOverScaleFactor = 1f;
613 setAccessibilityDelegate(new AccessibilityDelegate() {
615 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
616 super.onInitializeAccessibilityNodeInfo(host, info);
618 info.setClassName(FilmstripView.class.getName());
619 info.setScrollable(true);
620 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
621 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
625 public boolean performAccessibilityAction(View host, int action, Bundle args) {
626 if (!mController.isScrolling()) {
628 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
629 mController.goToNextItem();
632 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
633 boolean wentToPrevious = mController.goToPreviousItem();
634 if (!wentToPrevious) {
635 // at beginning of filmstrip, hide and go back to preview
636 mActivity.getCameraAppUI().hideFilmstrip();
640 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
641 // Prevent the view group itself from being selected.
642 // Instead, select the item in the center
643 final ViewItem currentItem = mViewItem[mCurrentItem];
644 currentItem.getView().performAccessibilityAction(action, args);
649 return super.performAccessibilityAction(host, action, args);
654 private void recycleView(View view, int dataId) {
655 final int viewType = (Integer) view.getTag(R.id.mediadata_tag_viewtype);
657 Queue<View> recycledViewsForType = recycledViews.get(viewType);
658 if (recycledViewsForType == null) {
659 recycledViewsForType = new ArrayDeque<View>();
660 recycledViews.put(viewType, recycledViewsForType);
662 recycledViewsForType.offer(view);
666 private View getRecycledView(int dataId) {
667 final int viewType = mDataAdapter.getItemViewType(dataId);
668 Queue<View> recycledViewsForType = recycledViews.get(viewType);
670 if (recycledViewsForType != null) {
671 result = recycledViewsForType.poll();
677 * Returns the controller.
679 * @return The {@code Controller}.
681 public FilmstripController getController() {
686 * Returns the draw area width of the current item.
688 public int getCurrentItemLeft() {
689 return mViewItem[mCurrentItem].getDrawAreaLeft();
692 private void setListener(FilmstripController.FilmstripListener l) {
696 private void setViewGap(int viewGap) {
697 mViewGapInPixel = viewGap;
701 * Called after current item or zoom level has changed.
703 public void zoomAtIndexChanged() {
704 if (mViewItem[mCurrentItem] == null) {
707 int id = mViewItem[mCurrentItem].getId();
708 mListener.onZoomAtIndexChanged(id, mScale);
712 * Checks if the data is at the center.
714 * @param id The id of the data to check.
715 * @return {@code True} if the data is currently at the center.
717 private boolean isDataAtCenter(int id) {
718 if (mViewItem[mCurrentItem] == null) {
721 if (mViewItem[mCurrentItem].getId() == id
722 && isCurrentItemCentered()) {
728 private void measureViewItem(ViewItem item, int boundWidth, int boundHeight) {
729 int id = item.getId();
730 ImageData imageData = mDataAdapter.getImageData(id);
731 if (imageData == null) {
732 Log.e(TAG, "trying to measure a null item");
736 Point dim = CameraUtil.resizeToFill(imageData.getWidth(), imageData.getHeight(),
737 imageData.getRotation(), boundWidth, boundHeight);
739 item.measure(MeasureSpec.makeMeasureSpec(dim.x, MeasureSpec.EXACTLY),
740 MeasureSpec.makeMeasureSpec(dim.y, MeasureSpec.EXACTLY));
744 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
745 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
747 int boundWidth = MeasureSpec.getSize(widthMeasureSpec);
748 int boundHeight = MeasureSpec.getSize(heightMeasureSpec);
749 if (boundWidth == 0 || boundHeight == 0) {
750 // Either width or height is unknown, can't measure children yet.
754 for (ViewItem item : mViewItem) {
756 measureViewItem(item, boundWidth, boundHeight);
761 mZoomView.measure(MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.EXACTLY),
762 MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.EXACTLY));
765 private int findTheNearestView(int pointX) {
768 // Find the first non-null ViewItem.
769 while (nearest < BUFFER_SIZE
770 && (mViewItem[nearest] == null || mViewItem[nearest].getLeftPosition() == -1)) {
773 // No existing available ViewItem
774 if (nearest == BUFFER_SIZE) {
778 int min = Math.abs(pointX - mViewItem[nearest].getCenterX());
780 for (int itemID = nearest + 1; itemID < BUFFER_SIZE && mViewItem[itemID] != null; itemID++) {
782 if (mViewItem[itemID].getLeftPosition() == -1) {
786 int c = mViewItem[itemID].getCenterX();
787 int dist = Math.abs(pointX - c);
796 private ViewItem buildItemFromData(int dataID) {
797 if (mActivity.isDestroyed()) {
798 // Loading item data is call from multiple AsyncTasks and the
799 // activity may be finished when buildItemFromData is called.
800 Log.d(TAG, "Activity destroyed, don't load data");
803 ImageData data = mDataAdapter.getImageData(dataID);
808 // Always scale by fixed filmstrip scale, since we only show items when
809 // in filmstrip. Preloading images with a different scale and bounds
810 // interferes with caching.
811 int width = Math.round(FILM_STRIP_SCALE * getWidth());
812 int height = Math.round(FILM_STRIP_SCALE * getHeight());
813 Log.v(TAG, "suggesting item bounds: " + width + "x" + height);
814 mDataAdapter.suggestViewSizeBound(width, height);
817 View recycled = getRecycledView(dataID);
818 View v = mDataAdapter.getView(mActivity, recycled, dataID);
822 ViewItem item = new ViewItem(dataID, v, data);
823 item.addViewToHierarchy();
827 private void checkItemAtMaxSize() {
828 ViewItem item = mViewItem[mCurrentItem];
829 if (item.isMaximumBitmapRequested()) {
832 item.setMaximumBitmapRequested();
833 // Request full size bitmap, or max that DataAdapter will create.
834 int id = item.getId();
835 int h = mDataAdapter.getImageData(id).getHeight();
836 int w = mDataAdapter.getImageData(id).getWidth();
837 item.resizeView(mActivity, w, h);
840 private void removeItem(int itemID) {
841 if (itemID >= mViewItem.length || mViewItem[itemID] == null) {
844 ImageData data = mDataAdapter.getImageData(mViewItem[itemID].getId());
846 Log.e(TAG, "trying to remove a null item");
849 mViewItem[itemID].removeViewFromHierarchy(false);
850 mViewItem[itemID] = null;
854 * We try to keep the one closest to the center of the screen at position
857 private void stepIfNeeded() {
858 if (!inFilmstrip() && !inFullScreen()) {
859 // The good timing to step to the next view is when everything is
860 // not in transition.
863 final int nearest = findTheNearestView(mCenterX);
865 if (nearest == -1 || nearest == mCurrentItem) {
868 int prevDataId = (mViewItem[mCurrentItem] == null ? -1 : mViewItem[mCurrentItem].getId());
869 final int adjust = nearest - mCurrentItem;
871 for (int k = 0; k < adjust; k++) {
874 for (int k = 0; k + adjust < BUFFER_SIZE; k++) {
875 mViewItem[k] = mViewItem[k + adjust];
877 for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) {
879 if (mViewItem[k - 1] != null) {
880 mViewItem[k] = buildItemFromData(mViewItem[k - 1].getId() + 1);
885 for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) {
888 for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) {
889 mViewItem[k] = mViewItem[k + adjust];
891 for (int k = -1 - adjust; k >= 0; k--) {
893 if (mViewItem[k + 1] != null) {
894 mViewItem[k] = buildItemFromData(mViewItem[k + 1].getId() - 1);
899 if (mListener != null) {
900 mListener.onDataFocusChanged(prevDataId, mViewItem[mCurrentItem].getId());
901 final int firstVisible = mViewItem[mCurrentItem].getId() - 2;
902 final int visibleItemCount = firstVisible + BUFFER_SIZE;
903 final int totalItemCount = mDataAdapter.getTotalNumber();
904 mListener.onScroll(firstVisible, visibleItemCount, totalItemCount);
906 zoomAtIndexChanged();
910 * Check the bounds of {@code mCenterX}. Always call this function after: 1.
911 * Any changes to {@code mCenterX}. 2. Any size change of the view items.
913 * @return Whether clamp happened.
915 private boolean clampCenterX() {
916 ViewItem curr = mViewItem[mCurrentItem];
921 boolean stopScroll = false;
922 if (curr.getId() == 1 && mCenterX < curr.getCenterX() && mDataIdOnUserScrolling > 1 &&
923 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY &&
924 mController.isScrolling()) {
927 if (curr.getId() == 0 && mCenterX < curr.getCenterX()) {
928 // Stop at the first ViewItem.
932 if (curr.getId() == mDataAdapter.getTotalNumber() - 1
933 && mCenterX > curr.getCenterX()) {
939 mCenterX = curr.getCenterX();
946 * Reorders the child views to be consistent with their data ID. This method
947 * should be called after adding/removing views.
949 private void adjustChildZOrder() {
950 for (int i = BUFFER_SIZE - 1; i >= 0; i--) {
951 if (mViewItem[i] == null) {
954 mViewItem[i].bringViewToFront();
956 // ZoomView is a special case to always be in the front. In L set to
957 // max elevation to make sure ZoomView is above other elevated views.
958 bringChildToFront(mZoomView);
959 if (ApiHelper.isLOrHigher()) {
960 setMaxElevation(mZoomView);
964 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
965 private void setMaxElevation(View v) {
966 v.setElevation(Float.MAX_VALUE);
970 * Returns the ID of the current item, or -1 if there is no data.
972 private int getCurrentId() {
973 ViewItem current = mViewItem[mCurrentItem];
974 if (current == null) {
977 return current.getId();
981 * Keep the current item in the center. This functions does not check if the
982 * current item is null.
984 private void snapInCenter() {
985 final ViewItem currItem = mViewItem[mCurrentItem];
986 if (currItem == null) {
989 final int currentViewCenter = currItem.getCenterX();
990 if (mController.isScrolling() || mIsUserScrolling
991 || isCurrentItemCentered()) {
995 int snapInTime = (int) (SNAP_IN_CENTER_TIME_MS
996 * ((float) Math.abs(mCenterX - currentViewCenter))
997 / mDrawArea.width());
998 mController.scrollToPosition(currentViewCenter,
1000 if (isViewTypeSticky(currItem) && !mController.isScaling() && mScale != FULL_SCREEN_SCALE) {
1001 // Now going to full screen camera
1002 mController.goToFullScreen();
1007 * Translates the {@link ViewItem} on the left of the current one to match
1008 * the full-screen layout. In full-screen, we show only one {@link ViewItem}
1009 * which occupies the whole screen. The other left ones are put on the left
1010 * side in full scales. Does nothing if there's no next item.
1012 * @param currItem The item ID of the current one to be translated.
1013 * @param drawAreaWidth The width of the current draw area.
1014 * @param scaleFraction A {@code float} between 0 and 1. 0 if the current
1015 * scale is {@code FILM_STRIP_SCALE}. 1 if the current scale is
1016 * {@code FULL_SCREEN_SCALE}.
1018 private void translateLeftViewItem(
1019 int currItem, int drawAreaWidth, float scaleFraction) {
1020 if (currItem < 0 || currItem > BUFFER_SIZE - 1) {
1021 Log.e(TAG, "currItem id out of bound.");
1025 final ViewItem curr = mViewItem[currItem];
1026 final ViewItem next = mViewItem[currItem + 1];
1027 if (curr == null || next == null) {
1028 Log.e(TAG, "Invalid view item (curr or next == null). curr = "
1033 final int currCenterX = curr.getCenterX();
1034 final int nextCenterX = next.getCenterX();
1035 final int translate = (int) ((nextCenterX - drawAreaWidth
1036 - currCenterX) * scaleFraction);
1038 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1040 curr.setVisibility(VISIBLE);
1042 if (inFullScreen()) {
1043 curr.setTranslationX(translate * (mCenterX - currCenterX) / (nextCenterX - currCenterX));
1045 curr.setTranslationX(translate);
1050 * Fade out the {@link ViewItem} on the right of the current one in
1051 * full-screen layout. Does nothing if there's no previous item.
1053 * @param currItemId The ID of the item to fade.
1055 private void fadeAndScaleRightViewItem(int currItemId) {
1056 if (currItemId < 1 || currItemId > BUFFER_SIZE) {
1057 Log.e(TAG, "currItem id out of bound.");
1061 final ViewItem currItem = mViewItem[currItemId];
1062 final ViewItem prevItem = mViewItem[currItemId - 1];
1063 if (currItem == null || prevItem == null) {
1064 Log.e(TAG, "Invalid view item (curr or prev == null). curr = "
1069 if (currItemId > mCurrentItem + 1) {
1070 // Every item not right next to the mCurrentItem is invisible.
1071 currItem.setVisibility(INVISIBLE);
1074 final int prevCenterX = prevItem.getCenterX();
1075 if (mCenterX <= prevCenterX) {
1076 // Shortcut. If the position is at the center of the previous one,
1077 // set to invisible too.
1078 currItem.setVisibility(INVISIBLE);
1081 final int currCenterX = currItem.getCenterX();
1082 final float fadeDownFraction =
1083 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX);
1084 currItem.layoutWithTranslationX(mDrawArea, currCenterX,
1085 FILM_STRIP_SCALE + (1f - FILM_STRIP_SCALE) * fadeDownFraction);
1086 currItem.setAlpha(fadeDownFraction);
1087 currItem.setTranslationX(0);
1088 currItem.setVisibility(VISIBLE);
1091 private void layoutViewItems(boolean layoutChanged) {
1092 if (mViewItem[mCurrentItem] == null ||
1093 mDrawArea.width() == 0 ||
1094 mDrawArea.height() == 0) {
1098 // If the layout changed, we need to adjust the current position so
1099 // that if an item is centered before the change, it's still centered.
1100 if (layoutChanged) {
1101 mViewItem[mCurrentItem].setLeftPosition(
1102 mCenterX - mViewItem[mCurrentItem].getMeasuredWidth() / 2);
1109 * Transformed scale fraction between 0 and 1. 0 if the scale is
1110 * {@link FILM_STRIP_SCALE}. 1 if the scale is {@link FULL_SCREEN_SCALE}
1113 final float scaleFraction = mViewAnimInterpolator.getInterpolation(
1114 (mScale - FILM_STRIP_SCALE) / (FULL_SCREEN_SCALE - FILM_STRIP_SCALE));
1115 final int fullScreenWidth = mDrawArea.width() + mViewGapInPixel;
1117 // Decide the position for all view items on the left and the right
1121 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
1122 final ViewItem curr = mViewItem[itemID];
1127 // First, layout relatively to the next one.
1128 final int currLeft = mViewItem[itemID + 1].getLeftPosition()
1129 - curr.getMeasuredWidth() - mViewGapInPixel;
1130 curr.setLeftPosition(currLeft);
1133 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
1134 final ViewItem curr = mViewItem[itemID];
1139 // First, layout relatively to the previous one.
1140 final ViewItem prev = mViewItem[itemID - 1];
1141 final int currLeft =
1142 prev.getLeftPosition() + prev.getMeasuredWidth()
1144 curr.setLeftPosition(currLeft);
1147 // Special case for the one immediately on the right of the camera
1149 boolean immediateRight =
1150 (mViewItem[mCurrentItem].getId() == 1 &&
1151 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY);
1153 // Layout the current ViewItem first.
1154 if (immediateRight) {
1155 // Just do a simple layout without any special translation or
1156 // fading. The implementation in Gallery does not push the first
1157 // photo to the bottom of the camera preview. Simply place the
1158 // photo on the right of the preview.
1159 final ViewItem currItem = mViewItem[mCurrentItem];
1160 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1161 currItem.setTranslationX(0f);
1162 currItem.setAlpha(1f);
1163 } else if (scaleFraction == 1f) {
1164 final ViewItem currItem = mViewItem[mCurrentItem];
1165 final int currCenterX = currItem.getCenterX();
1166 if (mCenterX < currCenterX) {
1167 // In full-screen and mCenterX is on the left of the center,
1168 // we draw the current one to "fade down".
1169 fadeAndScaleRightViewItem(mCurrentItem);
1170 } else if (mCenterX > currCenterX) {
1171 // In full-screen and mCenterX is on the right of the center,
1172 // we draw the current one translated.
1173 translateLeftViewItem(mCurrentItem, fullScreenWidth, scaleFraction);
1175 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1176 currItem.setTranslationX(0f);
1177 currItem.setAlpha(1f);
1180 final ViewItem currItem = mViewItem[mCurrentItem];
1181 // The normal filmstrip has no translation for the current item. If
1182 // it has translation before, gradually set it to zero.
1183 currItem.setTranslationX(currItem.getTranslationX() * scaleFraction);
1184 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1185 if (mViewItem[mCurrentItem - 1] == null) {
1186 currItem.setAlpha(1f);
1188 final int currCenterX = currItem.getCenterX();
1189 final int prevCenterX = mViewItem[mCurrentItem - 1].getCenterX();
1190 final float fadeDownFraction =
1191 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX);
1193 (1 - fadeDownFraction) * (1 - scaleFraction) + fadeDownFraction);
1197 // Layout the rest dependent on the current scale.
1199 // Items on the left
1200 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
1201 final ViewItem curr = mViewItem[itemID];
1205 translateLeftViewItem(itemID, fullScreenWidth, scaleFraction);
1208 // Items on the right
1209 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
1210 final ViewItem curr = mViewItem[itemID];
1215 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
1216 if (curr.getId() == 1 && isViewTypeSticky(curr)) {
1217 // Special case for the one next to the camera preview.
1222 if (scaleFraction == 1) {
1223 // It's in full-screen mode.
1224 fadeAndScaleRightViewItem(itemID);
1226 if (curr.getVisibility() == INVISIBLE) {
1227 curr.setVisibility(VISIBLE);
1229 if (itemID == mCurrentItem + 1) {
1230 curr.setAlpha(1f - scaleFraction);
1232 if (scaleFraction == 0f) {
1235 curr.setVisibility(INVISIBLE);
1238 curr.setTranslationX(
1239 (mViewItem[mCurrentItem].getLeftPosition() - curr.getLeftPosition()) *
1247 private boolean isViewTypeSticky(ViewItem item) {
1251 return mDataAdapter.getImageData(item.getId()).getViewType() ==
1252 ImageData.VIEW_TYPE_STICKY;
1256 public void onDraw(Canvas c) {
1257 // TODO: remove layoutViewItems() here.
1258 layoutViewItems(false);
1263 protected void onLayout(boolean changed, int l, int t, int r, int b) {
1266 mDrawArea.right = r - l;
1267 mDrawArea.bottom = b - t;
1268 mZoomView.layout(mDrawArea.left, mDrawArea.top, mDrawArea.right, mDrawArea.bottom);
1269 // TODO: Need a more robust solution to decide when to re-layout
1270 // If in the middle of zooming, only re-layout when the layout has
1272 if (!inZoomView() || changed) {
1274 layoutViewItems(changed);
1279 * Clears the translation and scale that has been set on the view, cancels
1280 * any loading request for image partial decoding, and hides zoom view. This
1281 * is needed for when there is a layout change (e.g. when users re-enter the
1282 * app, or rotate the device, etc).
1284 private void resetZoomView() {
1285 if (!inZoomView()) {
1288 ViewItem current = mViewItem[mCurrentItem];
1289 if (current == null) {
1292 mScale = FULL_SCREEN_SCALE;
1293 mController.cancelZoomAnimation();
1294 mController.cancelFlingAnimation();
1295 current.resetTransform();
1296 mController.cancelLoadingZoomedImage();
1297 mZoomView.setVisibility(GONE);
1298 mController.setSurroundingViewsVisible(true);
1301 private void hideZoomView() {
1303 mController.cancelLoadingZoomedImage();
1304 mZoomView.setVisibility(GONE);
1308 private void slideViewBack(ViewItem item) {
1309 item.animateTranslationX(0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1310 item.animateTranslationY(0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1311 item.animateAlpha(1f, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1314 private void animateItemRemoval(int dataID, final ImageData data) {
1315 if (mScale > FULL_SCREEN_SCALE) {
1318 int removedItemId = findItemByDataID(dataID);
1320 // adjust the data id to be consistent
1321 for (int i = 0; i < BUFFER_SIZE; i++) {
1322 if (mViewItem[i] == null || mViewItem[i].getId() <= dataID) {
1325 mViewItem[i].setId(mViewItem[i].getId() - 1);
1327 if (removedItemId == -1) {
1331 final ViewItem removedItem = mViewItem[removedItemId];
1332 final int offsetX = removedItem.getMeasuredWidth() + mViewGapInPixel;
1334 for (int i = removedItemId + 1; i < BUFFER_SIZE; i++) {
1335 if (mViewItem[i] != null) {
1336 mViewItem[i].setLeftPosition(mViewItem[i].getLeftPosition() - offsetX);
1340 if (removedItemId >= mCurrentItem
1341 && mViewItem[removedItemId].getId() < mDataAdapter.getTotalNumber()) {
1342 // Fill the removed item by left shift when the current one or
1343 // anyone on the right is removed, and there's more data on the
1345 for (int i = removedItemId; i < BUFFER_SIZE - 1; i++) {
1346 mViewItem[i] = mViewItem[i + 1];
1349 // pull data out from the DataAdapter for the last one.
1350 int curr = BUFFER_SIZE - 1;
1351 int prev = curr - 1;
1352 if (mViewItem[prev] != null) {
1353 mViewItem[curr] = buildItemFromData(mViewItem[prev].getId() + 1);
1356 // The animation part.
1357 if (inFullScreen()) {
1358 mViewItem[mCurrentItem].setVisibility(VISIBLE);
1359 ViewItem nextItem = mViewItem[mCurrentItem + 1];
1360 if (nextItem != null) {
1361 nextItem.setVisibility(INVISIBLE);
1365 // Translate the views to their original places.
1366 for (int i = removedItemId; i < BUFFER_SIZE; i++) {
1367 if (mViewItem[i] != null) {
1368 mViewItem[i].setTranslationX(offsetX);
1372 // The end of the filmstrip might have been changed.
1373 // The mCenterX might be out of the bound.
1374 ViewItem currItem = mViewItem[mCurrentItem];
1375 if(currItem!=null) {
1376 if (currItem.getId() == mDataAdapter.getTotalNumber() - 1
1377 && mCenterX > currItem.getCenterX()) {
1378 int adjustDiff = currItem.getCenterX() - mCenterX;
1379 mCenterX = currItem.getCenterX();
1380 for (int i = 0; i < BUFFER_SIZE; i++) {
1381 if (mViewItem[i] != null) {
1382 mViewItem[i].translateXScaledBy(adjustDiff);
1387 // CurrItem should NOT be NULL, but if is, at least don't crash.
1388 Log.w(TAG,"Caught invalid update in removal animation.");
1391 // fill the removed place by right shift
1392 mCenterX -= offsetX;
1394 for (int i = removedItemId; i > 0; i--) {
1395 mViewItem[i] = mViewItem[i - 1];
1398 // pull data out from the DataAdapter for the first one.
1400 int next = curr + 1;
1401 if (mViewItem[next] != null) {
1402 mViewItem[curr] = buildItemFromData(mViewItem[next].getId() - 1);
1405 // Translate the views to their original places.
1406 for (int i = removedItemId; i >= 0; i--) {
1407 if (mViewItem[i] != null) {
1408 mViewItem[i].setTranslationX(-offsetX);
1413 int transY = getHeight() / 8;
1414 if (removedItem.getTranslationY() < 0) {
1417 removedItem.animateTranslationY(removedItem.getTranslationY() + transY,
1418 GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1419 removedItem.animateAlpha(0f, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
1420 postDelayed(new Runnable() {
1423 removedItem.removeViewFromHierarchy(false);
1425 }, GEOMETRY_ADJUST_TIME_MS);
1427 adjustChildZOrder();
1430 // Now, slide every one back.
1431 if (mViewItem[mCurrentItem] == null) {
1434 for (int i = 0; i < BUFFER_SIZE; i++) {
1435 if (mViewItem[i] != null
1436 && mViewItem[i].getTranslationX() != 0f) {
1437 slideViewBack(mViewItem[i]);
1440 if (isCurrentItemCentered() && isViewTypeSticky(mViewItem[mCurrentItem])) {
1441 // Special case for scrolling onto the camera preview after removal.
1442 mController.goToFullScreen();
1446 // returns -1 on failure.
1447 private int findItemByDataID(int dataID) {
1448 for (int i = 0; i < BUFFER_SIZE; i++) {
1449 if (mViewItem[i] != null
1450 && mViewItem[i].getId() == dataID) {
1457 private void updateInsertion(int dataID) {
1458 int insertedItemId = findItemByDataID(dataID);
1459 if (insertedItemId == -1) {
1460 // Not in the current item buffers. Check if it's inserted
1462 if (dataID == mDataAdapter.getTotalNumber() - 1) {
1463 int prev = findItemByDataID(dataID - 1);
1464 if (prev >= 0 && prev < BUFFER_SIZE - 1) {
1465 // The previous data is in the buffer and we still
1466 // have room for the inserted data.
1467 insertedItemId = prev + 1;
1472 // adjust the data id to be consistent
1473 for (int i = 0; i < BUFFER_SIZE; i++) {
1474 if (mViewItem[i] == null || mViewItem[i].getId() < dataID) {
1477 mViewItem[i].setId(mViewItem[i].getId() + 1);
1479 if (insertedItemId == -1) {
1483 final ImageData data = mDataAdapter.getImageData(dataID);
1484 Point dim = CameraUtil
1485 .resizeToFill(data.getWidth(), data.getHeight(), data.getRotation(),
1486 getMeasuredWidth(), getMeasuredHeight());
1487 final int offsetX = dim.x + mViewGapInPixel;
1488 ViewItem viewItem = buildItemFromData(dataID);
1489 if (viewItem == null) {
1490 Log.w(TAG, "unable to build inserted item from data");
1494 if (insertedItemId >= mCurrentItem) {
1495 if (insertedItemId == mCurrentItem) {
1496 viewItem.setLeftPosition(mViewItem[mCurrentItem].getLeftPosition());
1498 // Shift right to make rooms for newly inserted item.
1499 removeItem(BUFFER_SIZE - 1);
1500 for (int i = BUFFER_SIZE - 1; i > insertedItemId; i--) {
1501 mViewItem[i] = mViewItem[i - 1];
1502 if (mViewItem[i] != null) {
1503 mViewItem[i].setTranslationX(-offsetX);
1504 slideViewBack(mViewItem[i]);
1508 // Shift left. Put the inserted data on the left instead of the
1511 if (insertedItemId < 0) {
1515 for (int i = 1; i <= insertedItemId; i++) {
1516 if (mViewItem[i] != null) {
1517 mViewItem[i].setTranslationX(offsetX);
1518 slideViewBack(mViewItem[i]);
1519 mViewItem[i - 1] = mViewItem[i];
1524 mViewItem[insertedItemId] = viewItem;
1525 viewItem.setAlpha(0f);
1526 viewItem.setTranslationY(getHeight() / 8);
1527 slideViewBack(viewItem);
1528 adjustChildZOrder();
1532 private void setDataAdapter(DataAdapter adapter) {
1533 mDataAdapter = adapter;
1534 int maxEdge = (int) (Math.max(this.getHeight(), this.getWidth())
1535 * FILM_STRIP_SCALE);
1536 mDataAdapter.suggestViewSizeBound(maxEdge, maxEdge);
1537 mDataAdapter.setListener(new DataAdapter.Listener() {
1539 public void onDataLoaded() {
1544 public void onDataUpdated(DataAdapter.UpdateReporter reporter) {
1549 public void onDataInserted(int dataId, ImageData data) {
1550 if (mViewItem[mCurrentItem] == null) {
1551 // empty now, simply do a reload.
1554 updateInsertion(dataId);
1556 if (mListener != null) {
1557 mListener.onDataFocusChanged(dataId, getCurrentId());
1562 public void onDataRemoved(int dataId, ImageData data) {
1563 animateItemRemoval(dataId, data);
1564 if (mListener != null) {
1565 mListener.onDataFocusChanged(dataId, getCurrentId());
1571 private boolean inFilmstrip() {
1572 return (mScale == FILM_STRIP_SCALE);
1575 private boolean inFullScreen() {
1576 return (mScale == FULL_SCREEN_SCALE);
1579 private boolean inZoomView() {
1580 return (mScale > FULL_SCREEN_SCALE);
1583 private boolean isCameraPreview() {
1584 return isViewTypeSticky(mViewItem[mCurrentItem]);
1587 private boolean inCameraFullscreen() {
1588 return isDataAtCenter(0) && inFullScreen()
1589 && (isViewTypeSticky(mViewItem[mCurrentItem]));
1593 public boolean onInterceptTouchEvent(MotionEvent ev) {
1594 if (mController.isScrolling()) {
1598 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
1599 mCheckToIntercept = true;
1600 mDown = MotionEvent.obtain(ev);
1601 ViewItem viewItem = mViewItem[mCurrentItem];
1602 // Do not intercept touch if swipe is not enabled
1603 if (viewItem != null && !mDataAdapter.canSwipeInFullScreen(viewItem.getId())) {
1604 mCheckToIntercept = false;
1607 } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
1608 // Do not intercept touch once child is in zoom mode
1609 mCheckToIntercept = false;
1612 if (!mCheckToIntercept) {
1615 if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) {
1618 int deltaX = (int) (ev.getX() - mDown.getX());
1619 int deltaY = (int) (ev.getY() - mDown.getY());
1620 if (ev.getActionMasked() == MotionEvent.ACTION_MOVE
1621 && deltaX < mSlop * (-1)) {
1622 // intercept left swipe
1623 if (Math.abs(deltaX) >= Math.abs(deltaY) * 2) {
1632 public boolean onTouchEvent(MotionEvent ev) {
1633 return mGestureRecognizer.onTouchEvent(ev);
1637 public boolean onGenericMotionEvent(MotionEvent ev) {
1638 mGestureRecognizer.onGenericMotionEvent(ev);
1642 FilmstripGestureRecognizer.Listener getGestureListener() {
1643 return mGestureListener;
1646 private void updateViewItem(int itemID) {
1647 ViewItem item = mViewItem[itemID];
1649 Log.e(TAG, "trying to update an null item");
1652 item.removeViewFromHierarchy(true);
1654 ViewItem newItem = buildItemFromData(item.getId());
1655 if (newItem == null) {
1656 Log.e(TAG, "new item is null");
1657 // keep using the old data.
1658 item.addViewToHierarchy();
1661 newItem.copyAttributes(item);
1662 mViewItem[itemID] = newItem;
1663 mZoomView.resetDecoder();
1665 boolean stopScroll = clampCenterX();
1667 mController.stopScrolling(true);
1669 adjustChildZOrder();
1671 if (mListener != null) {
1672 mListener.onDataUpdated(newItem.getId());
1676 /** Some of the data is changed. */
1677 private void update(DataAdapter.UpdateReporter reporter) {
1679 if (mViewItem[mCurrentItem] == null) {
1684 // Check the current one.
1685 ViewItem curr = mViewItem[mCurrentItem];
1686 int dataId = curr.getId();
1687 if (reporter.isDataRemoved(dataId)) {
1691 if (reporter.isDataUpdated(dataId)) {
1692 updateViewItem(mCurrentItem);
1693 final ImageData data = mDataAdapter.getImageData(dataId);
1694 if (!mIsUserScrolling && !mController.isScrolling()) {
1695 // If there is no scrolling at all, adjust mCenterX to place
1696 // the current item at the center.
1697 Point dim = CameraUtil.resizeToFill(data.getWidth(), data.getHeight(),
1698 data.getRotation(), getMeasuredWidth(), getMeasuredHeight());
1699 mCenterX = curr.getLeftPosition() + dim.x / 2;
1704 for (int i = mCurrentItem - 1; i >= 0; i--) {
1705 curr = mViewItem[i];
1707 dataId = curr.getId();
1708 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
1712 ViewItem next = mViewItem[i + 1];
1714 mViewItem[i] = buildItemFromData(next.getId() - 1);
1720 for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) {
1721 curr = mViewItem[i];
1723 dataId = curr.getId();
1724 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
1728 ViewItem prev = mViewItem[i - 1];
1730 mViewItem[i] = buildItemFromData(prev.getId() + 1);
1734 adjustChildZOrder();
1735 // Request a layout to find the measured width/height of the view first.
1737 // Update photo sphere visibility after metadata fully written.
1741 * The whole data might be totally different. Flush all and load from the
1742 * start. Filmstrip will be centered on the first item, i.e. the camera
1745 private void reload() {
1746 mController.stopScrolling(true);
1747 mController.stopScale();
1748 mDataIdOnUserScrolling = 0;
1751 if (mViewItem[mCurrentItem] != null) {
1752 prevId = mViewItem[mCurrentItem].getId();
1755 // Remove all views from the mViewItem buffer, except the camera view.
1756 for (int i = 0; i < mViewItem.length; i++) {
1757 if (mViewItem[i] == null) {
1760 mViewItem[i].removeViewFromHierarchy(false);
1763 // Clear out the mViewItems and rebuild with camera in the center.
1764 Arrays.fill(mViewItem, null);
1765 int dataNumber = mDataAdapter.getTotalNumber();
1766 if (dataNumber == 0) {
1770 mViewItem[mCurrentItem] = buildItemFromData(0);
1771 if (mViewItem[mCurrentItem] == null) {
1774 mViewItem[mCurrentItem].setLeftPosition(0);
1775 for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) {
1776 mViewItem[i] = buildItemFromData(mViewItem[i - 1].getId() + 1);
1777 if (mViewItem[i] == null) {
1782 // Ensure that the views in mViewItem will layout the first in the
1783 // center of the display upon a reload.
1785 mScale = FILM_STRIP_SCALE;
1787 adjustChildZOrder();
1790 if (mListener != null) {
1791 mListener.onDataReloaded();
1792 mListener.onDataFocusChanged(prevId, mViewItem[mCurrentItem].getId());
1796 private void promoteData(int itemID, int dataID) {
1797 if (mListener != null) {
1798 mListener.onFocusedDataPromoted(dataID);
1802 private void demoteData(int itemID, int dataID) {
1803 if (mListener != null) {
1804 mListener.onFocusedDataDemoted(dataID);
1808 private void onEnterFilmstrip() {
1809 if (mListener != null) {
1810 mListener.onEnterFilmstrip(getCurrentId());
1814 private void onLeaveFilmstrip() {
1815 if (mListener != null) {
1816 mListener.onLeaveFilmstrip(getCurrentId());
1820 private void onEnterFullScreen() {
1821 mFullScreenUIHidden = false;
1822 if (mListener != null) {
1823 mListener.onEnterFullScreenUiShown(getCurrentId());
1827 private void onLeaveFullScreen() {
1828 if (mListener != null) {
1829 mListener.onLeaveFullScreenUiShown(getCurrentId());
1833 private void onEnterFullScreenUiHidden() {
1834 mFullScreenUIHidden = true;
1835 if (mListener != null) {
1836 mListener.onEnterFullScreenUiHidden(getCurrentId());
1840 private void onLeaveFullScreenUiHidden() {
1841 mFullScreenUIHidden = false;
1842 if (mListener != null) {
1843 mListener.onLeaveFullScreenUiHidden(getCurrentId());
1847 private void onEnterZoomView() {
1848 if (mListener != null) {
1849 mListener.onEnterZoomView(getCurrentId());
1853 private void onLeaveZoomView() {
1854 mController.setSurroundingViewsVisible(true);
1858 * MyController controls all the geometry animations. It passively tells the
1859 * geometry information on demand.
1861 private class MyController implements FilmstripController {
1863 private final ValueAnimator mScaleAnimator;
1864 private ValueAnimator mZoomAnimator;
1865 private AnimatorSet mFlingAnimator;
1867 private final MyScroller mScroller;
1868 private boolean mCanStopScroll;
1870 private final MyScroller.Listener mScrollerListener =
1871 new MyScroller.Listener() {
1873 public void onScrollUpdate(int currX, int currY) {
1876 boolean stopScroll = clampCenterX();
1878 mController.stopScrolling(true);
1884 public void onScrollEnd() {
1885 mCanStopScroll = true;
1886 if (mViewItem[mCurrentItem] == null) {
1890 if (isCurrentItemCentered()
1891 && isViewTypeSticky(mViewItem[mCurrentItem])) {
1892 // Special case for the scrolling end on the camera
1899 private final ValueAnimator.AnimatorUpdateListener mScaleAnimatorUpdateListener =
1900 new ValueAnimator.AnimatorUpdateListener() {
1902 public void onAnimationUpdate(ValueAnimator animation) {
1903 if (mViewItem[mCurrentItem] == null) {
1906 mScale = (Float) animation.getAnimatedValue();
1911 MyController(Context context) {
1912 TimeInterpolator decelerateInterpolator = new DecelerateInterpolator(1.5f);
1913 mScroller = new MyScroller(mActivity,
1914 new Handler(mActivity.getMainLooper()),
1915 mScrollerListener, decelerateInterpolator);
1916 mCanStopScroll = true;
1918 mScaleAnimator = new ValueAnimator();
1919 mScaleAnimator.addUpdateListener(mScaleAnimatorUpdateListener);
1920 mScaleAnimator.setInterpolator(decelerateInterpolator);
1921 mScaleAnimator.addListener(new Animator.AnimatorListener() {
1923 public void onAnimationStart(Animator animator) {
1924 if (mScale == FULL_SCREEN_SCALE) {
1925 onLeaveFullScreen();
1927 if (mScale == FILM_STRIP_SCALE) {
1934 public void onAnimationEnd(Animator animator) {
1935 if (mScale == FULL_SCREEN_SCALE) {
1936 onEnterFullScreen();
1938 if (mScale == FILM_STRIP_SCALE) {
1942 zoomAtIndexChanged();
1946 public void onAnimationCancel(Animator animator) {
1951 public void onAnimationRepeat(Animator animator) {
1958 public void setImageGap(int imageGap) {
1959 FilmstripView.this.setViewGap(imageGap);
1963 public int getCurrentId() {
1964 return FilmstripView.this.getCurrentId();
1968 public void setDataAdapter(DataAdapter adapter) {
1969 FilmstripView.this.setDataAdapter(adapter);
1973 public boolean inFilmstrip() {
1974 return FilmstripView.this.inFilmstrip();
1978 public boolean inFullScreen() {
1979 return FilmstripView.this.inFullScreen();
1983 public boolean isCameraPreview() {
1984 return FilmstripView.this.isCameraPreview();
1988 public boolean inCameraFullscreen() {
1989 return FilmstripView.this.inCameraFullscreen();
1993 public void setListener(FilmstripListener l) {
1994 FilmstripView.this.setListener(l);
1998 public boolean isScrolling() {
1999 return !mScroller.isFinished();
2003 public boolean isScaling() {
2004 return mScaleAnimator.isRunning();
2007 private int estimateMinX(int dataID, int leftPos, int viewWidth) {
2008 return leftPos - (dataID + 100) * (viewWidth + mViewGapInPixel);
2011 private int estimateMaxX(int dataID, int leftPos, int viewWidth) {
2013 + (mDataAdapter.getTotalNumber() - dataID + 100)
2014 * (viewWidth + mViewGapInPixel);
2017 /** Zoom all the way in or out on the image at the given pivot point. */
2018 private void zoomAt(final ViewItem current, final float focusX, final float focusY) {
2019 // End previous zoom animation, if any
2020 if (mZoomAnimator != null) {
2021 mZoomAnimator.end();
2023 // Calculate end scale
2024 final float maxScale = getCurrentDataMaxScale(false);
2025 final float endScale = mScale < maxScale - maxScale * TOLERANCE
2026 ? maxScale : FULL_SCREEN_SCALE;
2028 mZoomAnimator = new ValueAnimator();
2029 mZoomAnimator.setFloatValues(mScale, endScale);
2030 mZoomAnimator.setDuration(ZOOM_ANIMATION_DURATION_MS);
2031 mZoomAnimator.addListener(new Animator.AnimatorListener() {
2033 public void onAnimationStart(Animator animation) {
2034 if (mScale == FULL_SCREEN_SCALE) {
2035 if (mFullScreenUIHidden) {
2036 onLeaveFullScreenUiHidden();
2038 onLeaveFullScreen();
2040 setSurroundingViewsVisible(false);
2041 } else if (inZoomView()) {
2044 cancelLoadingZoomedImage();
2048 public void onAnimationEnd(Animator animation) {
2049 // Make sure animation ends up having the correct scale even
2050 // if it is cancelled before it finishes
2051 if (mScale != endScale) {
2052 current.postScale(focusX, focusY, endScale / mScale, mDrawArea.width(),
2053 mDrawArea.height());
2057 if (inFullScreen()) {
2058 setSurroundingViewsVisible(true);
2059 mZoomView.setVisibility(GONE);
2060 current.resetTransform();
2061 onEnterFullScreenUiHidden();
2063 mController.loadZoomedImage();
2066 mZoomAnimator = null;
2067 zoomAtIndexChanged();
2071 public void onAnimationCancel(Animator animation) {
2076 public void onAnimationRepeat(Animator animation) {
2081 mZoomAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2083 public void onAnimationUpdate(ValueAnimator animation) {
2084 float newScale = (Float) animation.getAnimatedValue();
2085 float postScale = newScale / mScale;
2087 current.postScale(focusX, focusY, postScale, mDrawArea.width(),
2088 mDrawArea.height());
2091 mZoomAnimator.start();
2095 public void scroll(float deltaX) {
2096 if (!stopScrolling(false)) {
2101 boolean stopScroll = clampCenterX();
2103 mController.stopScrolling(true);
2109 public void fling(float velocityX) {
2110 if (!stopScrolling(false)) {
2113 final ViewItem item = mViewItem[mCurrentItem];
2118 float scaledVelocityX = velocityX / mScale;
2119 if (inFullScreen() && isViewTypeSticky(item) && scaledVelocityX < 0) {
2120 // Swipe left in camera preview.
2125 // Estimation of possible length on the left. To ensure the
2126 // velocity doesn't become too slow eventually, we add a huge number
2127 // to the estimated maximum.
2128 int minX = estimateMinX(item.getId(), item.getLeftPosition(), w);
2129 // Estimation of possible length on the right. Likewise, exaggerate
2130 // the possible maximum too.
2131 int maxX = estimateMaxX(item.getId(), item.getLeftPosition(), w);
2132 mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0);
2135 void flingInsideZoomView(float velocityX, float velocityY) {
2136 if (!inZoomView()) {
2140 final ViewItem current = mViewItem[mCurrentItem];
2141 if (current == null) {
2145 final int factor = DECELERATION_FACTOR;
2146 // Deceleration curve for distance:
2147 // S(t) = s + (e - s) * (1 - (1 - t/T) ^ factor)
2148 // Need to find the ending distance (e), so that the starting
2149 // velocity is the velocity of fling.
2150 // Velocity is the derivative of distance
2151 // V(t) = (e - s) * factor * (-1) * (1 - t/T) ^ (factor - 1) * (-1/T)
2152 // = (e - s) * factor * (1 - t/T) ^ (factor - 1) / T
2153 // Since V(0) = V0, we have e = T / factor * V0 + s
2155 // Duration T should be long enough so that at the end of the fling,
2156 // image moves at 1 pixel/s for about P = 50ms = 0.05s
2157 // i.e. V(T - P) = 1
2158 // V(T - P) = V0 * (1 - (T -P) /T) ^ (factor - 1) = 1
2159 // T = P * V0 ^ (1 / (factor -1))
2161 final float velocity = Math.max(Math.abs(velocityX), Math.abs(velocityY));
2162 // Dynamically calculate duration
2163 final float duration = (float) (FLING_COASTING_DURATION_S
2164 * Math.pow(velocity, (1f / (factor - 1f))));
2166 final float translationX = current.getTranslationX() * mScale;
2167 final float translationY = current.getTranslationY() * mScale;
2169 final ValueAnimator decelerationX = ValueAnimator.ofFloat(translationX,
2170 translationX + duration / factor * velocityX);
2171 final ValueAnimator decelerationY = ValueAnimator.ofFloat(translationY,
2172 translationY + duration / factor * velocityY);
2174 decelerationY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2176 public void onAnimationUpdate(ValueAnimator animation) {
2177 float transX = (Float) decelerationX.getAnimatedValue();
2178 float transY = (Float) decelerationY.getAnimatedValue();
2180 current.updateTransform(transX, transY, mScale,
2181 mScale, mDrawArea.width(), mDrawArea.height());
2185 mFlingAnimator = new AnimatorSet();
2186 mFlingAnimator.play(decelerationX).with(decelerationY);
2187 mFlingAnimator.setDuration((int) (duration * 1000));
2188 mFlingAnimator.setInterpolator(new TimeInterpolator() {
2190 public float getInterpolation(float input) {
2191 return (float) (1.0f - Math.pow((1.0f - input), factor));
2194 mFlingAnimator.addListener(new Animator.AnimatorListener() {
2195 private boolean mCancelled = false;
2198 public void onAnimationStart(Animator animation) {
2203 public void onAnimationEnd(Animator animation) {
2207 mFlingAnimator = null;
2211 public void onAnimationCancel(Animator animation) {
2216 public void onAnimationRepeat(Animator animation) {
2220 mFlingAnimator.start();
2224 public boolean stopScrolling(boolean forced) {
2225 if (!isScrolling()) {
2227 } else if (!mCanStopScroll && !forced) {
2230 mScroller.forceFinished(true);
2234 private void stopScale() {
2235 mScaleAnimator.cancel();
2239 public void scrollToPosition(int position, int duration, boolean interruptible) {
2240 if (mViewItem[mCurrentItem] == null) {
2243 mCanStopScroll = interruptible;
2244 mScroller.startScroll(mCenterX, 0, position - mCenterX, 0, duration);
2248 public boolean goToNextItem() {
2249 return goToItem(mCurrentItem + 1);
2253 public boolean goToPreviousItem() {
2254 return goToItem(mCurrentItem - 1);
2257 private boolean goToItem(int itemIndex) {
2258 final ViewItem nextItem = mViewItem[itemIndex];
2259 if (nextItem == null) {
2262 stopScrolling(true);
2263 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS * 2, false);
2265 if (isViewTypeSticky(mViewItem[mCurrentItem])) {
2266 // Special case when moving from camera preview.
2267 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
2272 private void scaleTo(float scale, int duration) {
2273 if (mViewItem[mCurrentItem] == null) {
2277 mScaleAnimator.setDuration(duration);
2278 mScaleAnimator.setFloatValues(mScale, scale);
2279 mScaleAnimator.start();
2283 public void goToFilmstrip() {
2284 if (mViewItem[mCurrentItem] == null) {
2287 if (mScale == FILM_STRIP_SCALE) {
2290 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
2292 final ViewItem currItem = mViewItem[mCurrentItem];
2293 final ViewItem nextItem = mViewItem[mCurrentItem + 1];
2294 if (currItem.getId() == 0 && isViewTypeSticky(currItem) && nextItem != null) {
2295 // Deal with the special case of swiping in camera preview.
2296 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, false);
2299 if (mScale == FILM_STRIP_SCALE) {
2305 public void goToFullScreen() {
2306 if (inFullScreen()) {
2310 scaleTo(FULL_SCREEN_SCALE, GEOMETRY_ADJUST_TIME_MS);
2313 private void cancelFlingAnimation() {
2314 // Cancels flinging for zoomed images
2315 if (isFlingAnimationRunning()) {
2316 mFlingAnimator.cancel();
2320 private void cancelZoomAnimation() {
2321 if (isZoomAnimationRunning()) {
2322 mZoomAnimator.cancel();
2326 private void setSurroundingViewsVisible(boolean visible) {
2327 // Hide everything on the left
2328 // TODO: Need to find a better way to toggle the visibility of views
2329 // around the current view.
2330 for (int i = 0; i < mCurrentItem; i++) {
2331 if (i == mCurrentItem || mViewItem[i] == null) {
2334 mViewItem[i].setVisibility(visible ? VISIBLE : INVISIBLE);
2338 private Uri getCurrentUri() {
2339 ViewItem curr = mViewItem[mCurrentItem];
2343 return mDataAdapter.getImageData(curr.getId()).getUri();
2347 * Here we only support up to 1:1 image zoom (i.e. a 100% view of the
2348 * actual pixels). The max scale that we can apply on the view should
2349 * make the view same size as the image, in pixels.
2351 private float getCurrentDataMaxScale(boolean allowOverScale) {
2352 ViewItem curr = mViewItem[mCurrentItem];
2354 return FULL_SCREEN_SCALE;
2356 ImageData imageData = mDataAdapter.getImageData(curr.getId());
2357 if (imageData == null || !imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) {
2358 return FULL_SCREEN_SCALE;
2360 float imageWidth = imageData.getWidth();
2361 if (imageData.getRotation() == 90
2362 || imageData.getRotation() == 270) {
2363 imageWidth = imageData.getHeight();
2365 float scale = imageWidth / curr.getWidth();
2366 if (allowOverScale) {
2367 // In addition to the scale we apply to the view for 100% view
2368 // (i.e. each pixel on screen corresponds to a pixel in image)
2369 // we allow scaling beyond that for better detail viewing.
2370 scale *= mOverScaleFactor;
2375 private void loadZoomedImage() {
2376 if (!inZoomView()) {
2379 ViewItem curr = mViewItem[mCurrentItem];
2383 ImageData imageData = mDataAdapter.getImageData(curr.getId());
2384 if (!imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) {
2387 Uri uri = getCurrentUri();
2388 RectF viewRect = curr.getViewRect();
2389 if (uri == null || uri == Uri.EMPTY) {
2392 int orientation = imageData.getRotation();
2393 mZoomView.loadBitmap(uri, orientation, viewRect);
2396 private void cancelLoadingZoomedImage() {
2397 mZoomView.cancelPartialDecodingTask();
2401 public void goToFirstItem() {
2402 if (mViewItem[mCurrentItem] == null) {
2406 // TODO: animate to camera if it is still in the mViewItem buffer
2407 // versus a full reload which will perform an immediate transition
2411 public boolean inZoomView() {
2412 return FilmstripView.this.inZoomView();
2415 public boolean isFlingAnimationRunning() {
2416 return mFlingAnimator != null && mFlingAnimator.isRunning();
2419 public boolean isZoomAnimationRunning() {
2420 return mZoomAnimator != null && mZoomAnimator.isRunning();
2424 private boolean isCurrentItemCentered() {
2425 return mViewItem[mCurrentItem].getCenterX() == mCenterX;
2428 private static class MyScroller {
2429 public interface Listener {
2430 public void onScrollUpdate(int currX, int currY);
2432 public void onScrollEnd();
2435 private final Handler mHandler;
2436 private final Listener mListener;
2438 private final Scroller mScroller;
2440 private final ValueAnimator mXScrollAnimator;
2441 private final Runnable mScrollChecker = new Runnable() {
2444 boolean newPosition = mScroller.computeScrollOffset();
2446 mListener.onScrollEnd();
2449 mListener.onScrollUpdate(mScroller.getCurrX(), mScroller.getCurrY());
2450 mHandler.removeCallbacks(this);
2451 mHandler.post(this);
2455 private final ValueAnimator.AnimatorUpdateListener mXScrollAnimatorUpdateListener =
2456 new ValueAnimator.AnimatorUpdateListener() {
2458 public void onAnimationUpdate(ValueAnimator animation) {
2459 mListener.onScrollUpdate((Integer) animation.getAnimatedValue(), 0);
2463 private final Animator.AnimatorListener mXScrollAnimatorListener =
2464 new Animator.AnimatorListener() {
2466 public void onAnimationCancel(Animator animation) {
2471 public void onAnimationEnd(Animator animation) {
2472 mListener.onScrollEnd();
2476 public void onAnimationRepeat(Animator animation) {
2481 public void onAnimationStart(Animator animation) {
2486 public MyScroller(Context ctx, Handler handler, Listener listener,
2487 TimeInterpolator interpolator) {
2489 mListener = listener;
2490 mScroller = new Scroller(ctx);
2491 mXScrollAnimator = new ValueAnimator();
2492 mXScrollAnimator.addUpdateListener(mXScrollAnimatorUpdateListener);
2493 mXScrollAnimator.addListener(mXScrollAnimatorListener);
2494 mXScrollAnimator.setInterpolator(interpolator);
2498 int startX, int startY,
2499 int velocityX, int velocityY,
2501 int minY, int maxY) {
2502 mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
2506 public void startScroll(int startX, int startY, int dx, int dy) {
2507 mScroller.startScroll(startX, startY, dx, dy);
2511 /** Only starts and updates scroll in x-axis. */
2512 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
2513 mXScrollAnimator.cancel();
2514 mXScrollAnimator.setDuration(duration);
2515 mXScrollAnimator.setIntValues(startX, startX + dx);
2516 mXScrollAnimator.start();
2519 public boolean isFinished() {
2520 return (mScroller.isFinished() && !mXScrollAnimator.isRunning());
2523 public void forceFinished(boolean finished) {
2524 mScroller.forceFinished(finished);
2526 mXScrollAnimator.cancel();
2530 private void runChecker() {
2531 if (mHandler == null || mListener == null) {
2534 mHandler.removeCallbacks(mScrollChecker);
2535 mHandler.post(mScrollChecker);
2539 private class MyGestureReceiver implements FilmstripGestureRecognizer.Listener {
2541 private static final int SCROLL_DIR_NONE = 0;
2542 private static final int SCROLL_DIR_VERTICAL = 1;
2543 private static final int SCROLL_DIR_HORIZONTAL = 2;
2544 // Indicating the current trend of scaling is up (>1) or down (<1).
2545 private float mScaleTrend;
2546 private float mMaxScale;
2547 private int mScrollingDirection = SCROLL_DIR_NONE;
2548 private long mLastDownTime;
2549 private float mLastDownY;
2552 public boolean onSingleTapUp(float x, float y) {
2553 ViewItem centerItem = mViewItem[mCurrentItem];
2554 if (inFilmstrip()) {
2555 if (centerItem != null && centerItem.areaContains(x, y)) {
2556 mController.goToFullScreen();
2559 } else if (inFullScreen()) {
2560 if (mFullScreenUIHidden) {
2561 onLeaveFullScreenUiHidden();
2562 onEnterFullScreen();
2564 onLeaveFullScreen();
2565 onEnterFullScreenUiHidden();
2573 public boolean onDoubleTap(float x, float y) {
2574 ViewItem current = mViewItem[mCurrentItem];
2575 if (current == null) {
2578 if (inFilmstrip()) {
2579 mController.goToFullScreen();
2581 } else if (mScale < FULL_SCREEN_SCALE || inCameraFullscreen()) {
2584 if (!mController.stopScrolling(false)) {
2587 if (inFullScreen()) {
2588 mController.zoomAt(current, x, y);
2589 checkItemAtMaxSize();
2591 } else if (mScale > FULL_SCREEN_SCALE) {
2593 mController.zoomAt(current, x, y);
2599 public boolean onDown(float x, float y) {
2600 mLastDownTime = SystemClock.uptimeMillis();
2602 mController.cancelFlingAnimation();
2603 if (!mController.stopScrolling(false)) {
2611 public boolean onUp(float x, float y) {
2612 ViewItem currItem = mViewItem[mCurrentItem];
2613 if (currItem == null) {
2616 if (mController.isZoomAnimationRunning() || mController.isFlingAnimationRunning()) {
2620 mController.loadZoomedImage();
2623 float promoteHeight = getHeight() * PROMOTE_HEIGHT_RATIO;
2624 float velocityPromoteHeight = getHeight() * VELOCITY_PROMOTE_HEIGHT_RATIO;
2625 mIsUserScrolling = false;
2626 mScrollingDirection = SCROLL_DIR_NONE;
2627 // Finds items promoted/demoted.
2628 float speedY = Math.abs(y - mLastDownY)
2629 / (SystemClock.uptimeMillis() - mLastDownTime);
2630 for (int i = 0; i < BUFFER_SIZE; i++) {
2631 if (mViewItem[i] == null) {
2634 float transY = mViewItem[i].getTranslationY();
2638 int id = mViewItem[i].getId();
2640 if (mDataAdapter.getImageData(id)
2641 .isUIActionSupported(ImageData.ACTION_DEMOTE)
2642 && ((transY > promoteHeight)
2643 || (transY > velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) {
2645 } else if (mDataAdapter.getImageData(id)
2646 .isUIActionSupported(ImageData.ACTION_PROMOTE)
2647 && (transY < -promoteHeight
2648 || (transY < -velocityPromoteHeight && speedY > PROMOTE_VELOCITY))) {
2651 // put the view back.
2652 slideViewBack(mViewItem[i]);
2656 // The data might be changed. Re-check.
2657 currItem = mViewItem[mCurrentItem];
2658 if (currItem == null) {
2662 int currId = currItem.getId();
2663 if (mCenterX > currItem.getCenterX() + CAMERA_PREVIEW_SWIPE_THRESHOLD && currId == 0 &&
2664 isViewTypeSticky(currItem) && mDataIdOnUserScrolling == 0) {
2665 mController.goToFilmstrip();
2666 // Special case to go from camera preview to the next photo.
2667 if (mViewItem[mCurrentItem + 1] != null) {
2668 mController.scrollToPosition(
2669 mViewItem[mCurrentItem + 1].getCenterX(),
2670 GEOMETRY_ADJUST_TIME_MS, false);
2676 if (isCurrentItemCentered() && currId == 0 && isViewTypeSticky(currItem)) {
2677 mController.goToFullScreen();
2679 if (mDataIdOnUserScrolling == 0 && currId != 0) {
2680 // Special case to go to filmstrip when the user scroll away
2681 // from the camera preview and the current one is not the
2683 mController.goToFilmstrip();
2684 mDataIdOnUserScrolling = currId;
2692 public void onLongPress(float x, float y) {
2693 final int dataId = getCurrentId();
2697 mListener.onFocusedDataLongPressed(dataId);
2701 public boolean onScroll(float x, float y, float dx, float dy) {
2702 final ViewItem currItem = mViewItem[mCurrentItem];
2703 if (currItem == null) {
2706 if (inFullScreen() && !mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
2710 // When image is zoomed in to be bigger than the screen
2712 ViewItem curr = mViewItem[mCurrentItem];
2713 float transX = curr.getTranslationX() * mScale - dx;
2714 float transY = curr.getTranslationY() * mScale - dy;
2715 curr.updateTransform(transX, transY, mScale, mScale, mDrawArea.width(),
2716 mDrawArea.height());
2719 int deltaX = (int) (dx / mScale);
2720 // Forces the current scrolling to stop.
2721 mController.stopScrolling(true);
2722 if (!mIsUserScrolling) {
2723 mIsUserScrolling = true;
2724 mDataIdOnUserScrolling = mViewItem[mCurrentItem].getId();
2726 if (inFilmstrip()) {
2727 // Disambiguate horizontal/vertical first.
2728 if (mScrollingDirection == SCROLL_DIR_NONE) {
2729 mScrollingDirection = (Math.abs(dx) > Math.abs(dy)) ? SCROLL_DIR_HORIZONTAL :
2730 SCROLL_DIR_VERTICAL;
2732 if (mScrollingDirection == SCROLL_DIR_HORIZONTAL) {
2733 if (mCenterX == currItem.getCenterX() && currItem.getId() == 0 && dx < 0) {
2734 // Already at the beginning, don't process the swipe.
2735 mIsUserScrolling = false;
2736 mScrollingDirection = SCROLL_DIR_NONE;
2739 mController.scroll(deltaX);
2741 // Vertical part. Promote or demote.
2743 Rect hitRect = new Rect();
2744 for (; hit < BUFFER_SIZE; hit++) {
2745 if (mViewItem[hit] == null) {
2748 mViewItem[hit].getHitRect(hitRect);
2749 if (hitRect.contains((int) x, (int) y)) {
2753 if (hit == BUFFER_SIZE) {
2758 ImageData data = mDataAdapter.getImageData(mViewItem[hit].getId());
2759 float transY = mViewItem[hit].getTranslationY() - dy / mScale;
2760 if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) &&
2764 if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) &&
2768 mViewItem[hit].setTranslationY(transY);
2770 } else if (inFullScreen()) {
2771 if (mViewItem[mCurrentItem] == null || (deltaX < 0 && mCenterX <=
2772 currItem.getCenterX() && currItem.getId() == 0)) {
2775 // Multiplied by 1.2 to make it more easy to swipe.
2776 mController.scroll((int) (deltaX * 1.2));
2784 public boolean onMouseScroll(float hscroll, float vscroll) {
2787 hscroll *= MOUSE_SCROLL_FACTOR;
2788 vscroll *= MOUSE_SCROLL_FACTOR;
2790 if (vscroll != 0f) {
2796 if (inFullScreen()) {
2797 onFling(-scroll, 0f);
2798 } else if (inZoomView()) {
2799 onScroll(0f, 0f, hscroll, vscroll);
2801 onScroll(0f, 0f, scroll, 0f);
2808 public boolean onFling(float velocityX, float velocityY) {
2809 final ViewItem currItem = mViewItem[mCurrentItem];
2810 if (currItem == null) {
2813 if (!mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
2817 // Fling within the zoomed image
2818 mController.flingInsideZoomView(velocityX, velocityY);
2821 if (Math.abs(velocityX) < Math.abs(velocityY)) {
2822 // ignore vertical fling.
2826 // In full-screen, fling of a velocity above a threshold should go
2827 // to the next/prev photos
2828 if (mScale == FULL_SCREEN_SCALE) {
2829 int currItemCenterX = currItem.getCenterX();
2831 if (velocityX > 0) { // left
2832 if (mCenterX > currItemCenterX) {
2833 // The visually previous item is actually the current
2835 mController.scrollToPosition(
2836 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
2839 ViewItem prevItem = mViewItem[mCurrentItem - 1];
2840 if (prevItem == null) {
2843 mController.scrollToPosition(
2844 prevItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true);
2846 if (mController.stopScrolling(false)) {
2847 if (mCenterX < currItemCenterX) {
2848 // The visually next item is actually the current
2850 mController.scrollToPosition(
2851 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
2854 final ViewItem nextItem = mViewItem[mCurrentItem + 1];
2855 if (nextItem == null) {
2858 mController.scrollToPosition(
2859 nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true);
2860 if (isViewTypeSticky(currItem)) {
2861 mController.goToFilmstrip();
2867 if (mScale == FILM_STRIP_SCALE) {
2868 mController.fling(velocityX);
2874 public boolean onScaleBegin(float focusX, float focusY) {
2875 if (inCameraFullscreen()) {
2881 // If the image is smaller than screen size, we should allow to zoom
2882 // in to full screen size
2883 mMaxScale = Math.max(mController.getCurrentDataMaxScale(true), FULL_SCREEN_SCALE);
2888 public boolean onScale(float focusX, float focusY, float scale) {
2889 if (inCameraFullscreen()) {
2893 mScaleTrend = mScaleTrend * 0.3f + scale * 0.7f;
2894 float newScale = mScale * scale;
2895 if (mScale < FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) {
2896 if (newScale <= FILM_STRIP_SCALE) {
2897 newScale = FILM_STRIP_SCALE;
2899 // Scaled view is smaller than or equal to screen size both
2900 // before and after scaling
2901 if (mScale != newScale) {
2902 if (mScale == FILM_STRIP_SCALE) {
2905 if (newScale == FILM_STRIP_SCALE) {
2911 } else if (mScale < FULL_SCREEN_SCALE && newScale >= FULL_SCREEN_SCALE) {
2912 // Going from smaller than screen size to bigger than or equal
2914 if (mScale == FILM_STRIP_SCALE) {
2917 mScale = FULL_SCREEN_SCALE;
2918 onEnterFullScreen();
2919 mController.setSurroundingViewsVisible(false);
2921 } else if (mScale >= FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) {
2922 // Going from bigger than or equal to screen size to smaller
2924 if (inFullScreen()) {
2925 if (mFullScreenUIHidden) {
2926 onLeaveFullScreenUiHidden();
2928 onLeaveFullScreen();
2937 // Scaled view bigger than or equal to screen size both before
2938 // and after scaling
2939 if (!inZoomView()) {
2940 mController.setSurroundingViewsVisible(false);
2942 ViewItem curr = mViewItem[mCurrentItem];
2943 // Make sure the image is not overly scaled
2944 newScale = Math.min(newScale, mMaxScale);
2945 if (newScale == mScale) {
2948 float postScale = newScale / mScale;
2949 curr.postScale(focusX, focusY, postScale, mDrawArea.width(), mDrawArea.height());
2951 if (mScale == FULL_SCREEN_SCALE) {
2952 onEnterFullScreen();
2956 checkItemAtMaxSize();
2962 public void onScaleEnd() {
2963 zoomAtIndexChanged();
2964 if (mScale > FULL_SCREEN_SCALE + TOLERANCE) {
2967 mController.setSurroundingViewsVisible(true);
2968 if (mScale <= FILM_STRIP_SCALE + TOLERANCE) {
2969 mController.goToFilmstrip();
2970 } else if (mScaleTrend > 1f || mScale > FULL_SCREEN_SCALE - TOLERANCE) {
2972 mScale = FULL_SCREEN_SCALE;
2975 mController.goToFullScreen();
2977 mController.goToFilmstrip();