From 3eb0acbe1fcdcd10e962abaab7d946c6013343b3 Mon Sep 17 00:00:00 2001 From: Angus Kong Date: Fri, 5 Apr 2013 11:17:19 -0700 Subject: Add more gestures and animations in FilmStripView. Now supports fullscreen mode and filmstrip mode. Change-Id: I4c568e37f3306d970b3439f86ac6757b15c7cd4f --- src/com/android/camera/data/CameraDataAdapter.java | 25 +- .../camera/ui/FilmStripGestureRecognizer.java | 107 ++++ src/com/android/camera/ui/FilmStripView.java | 543 ++++++++++++++++----- 3 files changed, 557 insertions(+), 118 deletions(-) create mode 100644 src/com/android/camera/ui/FilmStripGestureRecognizer.java diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java index 5d10953f8..7a75a6d3d 100644 --- a/src/com/android/camera/data/CameraDataAdapter.java +++ b/src/com/android/camera/data/CameraDataAdapter.java @@ -33,6 +33,7 @@ import android.widget.ImageView; import com.android.camera.Storage; import com.android.camera.ui.FilmStripView; +import com.android.camera.ui.FilmStripView.ImageData; import java.util.ArrayList; import java.util.List; @@ -79,7 +80,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { private List mImages; - private FilmStripView mFilmStripView; + private Listener mListener; private View mCameraPreviewView; private ColorDrawable mPlaceHolder; @@ -101,7 +102,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { } @Override - public FilmStripView.ImageData getImageData(int id) { + public ImageData getImageData(int id) { if (id >= mImages.size()) return null; return mImages.get(id); } @@ -142,8 +143,8 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { } @Override - public void setDataListener(FilmStripView v) { - mFilmStripView = v; + public void setListener(Listener listener) { + mListener = listener; } private LocalImageData buildCameraImageData(int width, int height, int orientation) { @@ -152,6 +153,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { d.height = height; d.orientation = orientation; d.isCameraData = true; + d.supportedAction = ImageData.ACTION_NONE; return d; } @@ -179,6 +181,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { d.orientation = c.getInt(COL_ORIENTATION); d.width = c.getInt(COL_WIDTH); d.height = c.getInt(COL_HEIGHT); + d.supportedAction = ImageData.ACTION_PROMOTE | ImageData.ACTION_DEMOTE; if (d.width <= 0 || d.height <= 0) { Log.v(TAG, "warning! zero dimension for " + d.path + ":" + d.width + "x" + d.height); @@ -229,6 +232,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { // width and height should be adjusted according to orientation. public int width; public int height; + public int supportedAction; @Override public int getWidth() { @@ -240,6 +244,17 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { return height; } + @Override + public int getType() { + if (isCameraData) return ImageData.TYPE_CAMERA_PREVIEW; + return ImageData.TYPE_PHOTO; + } + + @Override + public boolean isActionSupported(int action) { + return ((action & supportedAction) != 0); + } + @Override public String toString() { return "LocalImageData:" + ",data=" + path + ",mimeType=" + mimeType @@ -281,7 +296,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { mImages = l; if (first != null) addOrReplaceCameraData(first); // both might be null. - if (changed) mFilmStripView.onDataChanged(); + if (changed && mListener != null) mListener.onDataLoaded(); } } diff --git a/src/com/android/camera/ui/FilmStripGestureRecognizer.java b/src/com/android/camera/ui/FilmStripGestureRecognizer.java new file mode 100644 index 000000000..f0e2534d3 --- /dev/null +++ b/src/com/android/camera/ui/FilmStripGestureRecognizer.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +// This class aggregates three gesture detectors: GestureDetector, +// ScaleGestureDetector. +public class FilmStripGestureRecognizer { + @SuppressWarnings("unused") + private static final String TAG = "FilmStripGestureRecognizer"; + + public interface Listener { + boolean onSingleTapUp(float x, float y); + boolean onDoubleTap(float x, float y); + boolean onScroll(float x, float y, float dx, float dy); + boolean onFling(float velocityX, float velocityY); + boolean onScaleBegin(float focusX, float focusY); + boolean onScale(float focusX, float focusY, float scale); + boolean onDown(float x, float y); + void onScaleEnd(); + } + + private final GestureDetector mGestureDetector; + private final ScaleGestureDetector mScaleDetector; + private final Listener mListener; + + public FilmStripGestureRecognizer(Context context, Listener listener) { + mListener = listener; + mGestureDetector = new GestureDetector(context, new MyGestureListener(), + null, true /* ignoreMultitouch */); + mScaleDetector = new ScaleGestureDetector( + context, new MyScaleListener()); + } + + public boolean onTouchEvent(MotionEvent event) { + return mGestureDetector.onTouchEvent(event) || mScaleDetector.onTouchEvent(event); + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return mListener.onSingleTapUp(e.getX(), e.getY()); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + return mListener.onDoubleTap(e.getX(), e.getY()); + } + + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + return mListener.onScroll(e2.getX(), e2.getY(), dx, dy); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return mListener.onFling(velocityX, velocityY); + } + + @Override + public boolean onDown(MotionEvent e) { + mListener.onDown(e.getX(), e.getY()); + return super.onDown(e); + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return mListener.onScaleBegin( + detector.getFocusX(), detector.getFocusY()); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + return mListener.onScale(detector.getFocusX(), + detector.getFocusY(), detector.getScaleFactor()); + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mListener.onScaleEnd(); + } + } +} diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java index 326e969b2..f2ffa8719 100644 --- a/src/com/android/camera/ui/FilmStripView.java +++ b/src/com/android/camera/ui/FilmStripView.java @@ -16,46 +16,72 @@ package com.android.camera.ui; +import android.animation.Animator; +import android.animation.ValueAnimator; import android.content.ContentResolver; import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.Log; -import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; import android.widget.Scroller; public class FilmStripView extends ViewGroup { - private static final String TAG = "FilmStripView"; private static final int BUFFER_SIZE = 5; // Horizontal padding of children. private static final int H_PADDING = 50; // Duration to go back to the first. - private static final int BACK_SCROLL_DURATION = 500; - private static final float MIN_SCALE = 0.7f; + private static final int DURATION_BACK_ANIM = 500; + private static final int DURATION_SCROLL_TO_FILMSTRIP = 350; + private static final int DURATION_GEOMETRY_ADJUST = 200; + private static final float FILM_STRIP_SCALE = 0.6f; + private static final float MAX_SCALE = 1f; private Context mContext; - private GestureDetector mGestureDetector; + private FilmStripGestureRecognizer mGestureRecognizer; private DataAdapter mDataAdapter; private final Rect mDrawArea = new Rect(); private int mCurrentInfo; - private Scroller mScroller; - private boolean mIsScrolling; + private float mScale; + private GeometryAnimator mGeometryAnimator; private int mCenterPosition = -1; private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE]; public interface ImageData { + public static final int TYPE_NONE = 0; + public static final int TYPE_CAMERA_PREVIEW = 1; + public static final int TYPE_PHOTO = 2; + public static final int TYPE_VIDEO = 3; + public static final int TYPE_PHOTOSPHERE = 4; + + // The actions are defined bit-wise so we can use bit operations like + // | and &. + public static final int ACTION_NONE = 0; + public static final int ACTION_PROMOTE = 1; + public static final int ACTION_DEMOTE = 2; + + // SIZE_FULL means disgard the width or height when deciding the view size + // of this ImageData, just use full screen size. + public static final int SIZE_FULL = -2; + // The values returned by getWidth() and getHeight() will be used for layout. public int getWidth(); public int getHeight(); + public int getType(); + public boolean isActionSupported(int action); } public interface DataAdapter { + public interface Listener { + public void onDataLoaded(); + public void onDataInserted(int dataID); + public void onDataRemoved(int dataID); + } public int getTotalNumber(); public View getView(Context context, int id); @@ -63,22 +89,27 @@ public class FilmStripView extends ViewGroup { public void suggestSize(int w, int h); public void requestLoad(ContentResolver r); - public void setDataListener(FilmStripView v); + public void setListener(Listener listener); } + // A helper class to tract and calculate the view coordination. private static class ViewInfo { private int mDataID; // the position of the left of the view in the whole filmstrip. private int mLeftPosition; - private View mView; + private View mView; + private float mOffsetY; public ViewInfo(int id, View v) { + v.setPivotX(0f); + v.setPivotY(0f); mDataID = id; mView = v; mLeftPosition = -1; + mOffsetY = 0; } - public int getId() { + public int getID() { return mDataID; } @@ -90,7 +121,15 @@ public class FilmStripView extends ViewGroup { return mLeftPosition; } - public int getCenterPosition() { + public float getOffsetY() { + return mOffsetY; + } + + public void setOffsetY(float offset) { + mOffsetY = offset; + } + + public int getCenterX() { return mLeftPosition + mView.getWidth() / 2; } @@ -98,15 +137,20 @@ public class FilmStripView extends ViewGroup { return mView; } - private void layoutAt(int l, int t) { - mView.layout(l, t, l + mView.getMeasuredWidth(), t + mView.getMeasuredHeight()); + private void layoutAt(int left, int top) { + mView.layout(left, top, left + mView.getMeasuredWidth(), + top + mView.getMeasuredHeight()); } - public void layoutIn(Rect drawArea, int refCenter) { + public void layoutIn(Rect drawArea, int refCenter, float scale) { // drawArea is where to layout in. // refCenter is the absolute horizontal position of the center of drawArea. - layoutAt(drawArea.centerX() + mLeftPosition - refCenter, - drawArea.centerY() - mView.getMeasuredHeight() / 2); + int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter) * scale); + int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale + + mOffsetY); + layoutAt(left, top); + mView.setScaleX(scale); + mView.setScaleY(scale); } } @@ -129,15 +173,33 @@ public class FilmStripView extends ViewGroup { mCurrentInfo = (BUFFER_SIZE - 1) / 2; setWillNotDraw(false); mContext = context; - mScroller = new Scroller(context); - mGestureDetector = - new GestureDetector(context, new MyGestureListener(), - null, true /* ignoreMultitouch */); + mScale = 1.0f; + mGeometryAnimator = new GeometryAnimator(context); + mGestureRecognizer = + new FilmStripGestureRecognizer(context, new MyGestureReceiver()); + } + + public float getScale() { + return mScale; + } + + public boolean isAnchoredTo(int id) { + if (mViewInfo[mCurrentInfo].getID() == id + && mViewInfo[mCurrentInfo].getCenterX() == mCenterPosition) { + return true; + } + return false; + } + + public int getCurrentType() { + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr == null) return ImageData.TYPE_NONE; + return mDataAdapter.getImageData(curr.getID()).getType(); } @Override public void onDraw(Canvas c) { - if (mIsScrolling) { + if (mGeometryAnimator.hasNewGeometry()) { layoutChildren(); } } @@ -146,13 +208,11 @@ public class FilmStripView extends ViewGroup { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - int w = MeasureSpec.getSize(widthMeasureSpec); - int h = MeasureSpec.getSize(heightMeasureSpec); - float scale = MIN_SCALE; - if (mDataAdapter != null) mDataAdapter.suggestSize(w / 2, h / 2); - - int boundWidth = (int) (w * scale); - int boundHeight = (int) (h * scale); + int boundWidth = MeasureSpec.getSize(widthMeasureSpec); + int boundHeight = MeasureSpec.getSize(heightMeasureSpec); + if (mDataAdapter != null) { + mDataAdapter.suggestSize(boundWidth / 2, boundHeight / 2); + } int wMode = View.MeasureSpec.EXACTLY; int hMode = View.MeasureSpec.EXACTLY; @@ -161,22 +221,25 @@ public class FilmStripView extends ViewGroup { ViewInfo info = mViewInfo[i]; if (mViewInfo[i] == null) continue; - int imageWidth = mDataAdapter.getImageData(info.getId()).getWidth(); - int imageHeight = mDataAdapter.getImageData(info.getId()).getHeight(); + int imageWidth = mDataAdapter.getImageData(info.getID()).getWidth(); + int imageHeight = mDataAdapter.getImageData(info.getID()).getHeight(); + if (imageWidth == ImageData.SIZE_FULL) imageWidth = boundWidth; + if (imageHeight == ImageData.SIZE_FULL) imageHeight = boundHeight; int scaledWidth = boundWidth; int scaledHeight = boundHeight; + if (imageWidth * scaledHeight > scaledWidth * imageHeight) { scaledHeight = imageHeight * scaledWidth / imageWidth; } else { scaledWidth = imageWidth * scaledHeight / imageHeight; } - scaledWidth += H_PADDING * 2 * scale; + scaledWidth += H_PADDING * 2; mViewInfo[i].getView().measure( View.MeasureSpec.makeMeasureSpec(scaledWidth, wMode) , View.MeasureSpec.makeMeasureSpec(scaledHeight, hMode)); } - setMeasuredDimension(w, h); + setMeasuredDimension(boundWidth, boundHeight); } private int findTheNearestView(int pointX) { @@ -188,14 +251,14 @@ public class FilmStripView extends ViewGroup { nearest++); // no existing available ViewInfo if (nearest == BUFFER_SIZE) return -1; - int min = Math.abs(pointX - mViewInfo[nearest].getCenterPosition()); + int min = Math.abs(pointX - mViewInfo[nearest].getCenterX()); for (int infoID = nearest + 1; infoID < BUFFER_SIZE && mViewInfo[infoID] != null; infoID++) { // not measured yet. if (mViewInfo[infoID].getLeftPosition() == -1) continue; - int c = mViewInfo[infoID].getCenterPosition(); + int c = mViewInfo[infoID].getCenterX(); int dist = Math.abs(pointX - c); if (dist < min) { min = dist; @@ -205,6 +268,14 @@ public class FilmStripView extends ViewGroup { return nearest; } + private ViewInfo buildInfoFromData(int dataID) { + View v = mDataAdapter.getView(mContext, dataID); + if (v == null) return null; + v.setPadding(H_PADDING, 0, H_PADDING, 0); + addView(v); + return new ViewInfo(dataID, v); + } + // We try to keep the one closest to the center of the screen at position mCurrentInfo. private void stepIfNeeded() { int nearest = findTheNearestView(mCenterPosition); @@ -223,7 +294,8 @@ public class FilmStripView extends ViewGroup { } for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) { mViewInfo[k] = null; - if (mViewInfo[k - 1] != null) getInfo(k, mViewInfo[k - 1].getId() + 1); + if (mViewInfo[k - 1] != null) + mViewInfo[k] = buildInfoFromData(mViewInfo[k - 1].getID() + 1); } } else { for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) { @@ -236,47 +308,46 @@ public class FilmStripView extends ViewGroup { } for (int k = -1 - adjust; k >= 0; k--) { mViewInfo[k] = null; - if (mViewInfo[k + 1] != null) getInfo(k, mViewInfo[k + 1].getId() - 1); + if (mViewInfo[k + 1] != null) + mViewInfo[k] = buildInfoFromData(mViewInfo[k + 1].getID() - 1); } } } - private void stopScroll() { - mScroller.forceFinished(true); - mIsScrolling = false; - } - + // Don't go out of bound. private void adjustCenterPosition() { ViewInfo curr = mViewInfo[mCurrentInfo]; if (curr == null) return; - if (curr.getId() == 0 && mCenterPosition < curr.getCenterPosition()) { - mCenterPosition = curr.getCenterPosition(); - if (mIsScrolling) stopScroll(); + if (curr.getID() == 0 && mCenterPosition < curr.getCenterX()) { + mCenterPosition = curr.getCenterX(); + mGeometryAnimator.stopScroll(); } - if (curr.getId() == mDataAdapter.getTotalNumber() - 1 - && mCenterPosition > curr.getCenterPosition()) { - mCenterPosition = curr.getCenterPosition(); - if (mIsScrolling) stopScroll(); + if (curr.getID() == mDataAdapter.getTotalNumber() - 1 + && mCenterPosition > curr.getCenterX()) { + mCenterPosition = curr.getCenterX(); + mGeometryAnimator.stopScroll(); } } private void layoutChildren() { - mIsScrolling = mScroller.computeScrollOffset(); - - if (mIsScrolling) mCenterPosition = mScroller.getCurrX(); + if (mGeometryAnimator.hasNewGeometry()) { + mCenterPosition = mGeometryAnimator.getNewPosition(); + mScale = mGeometryAnimator.getNewScale(); + } adjustCenterPosition(); - mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterPosition); + mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterPosition, mScale); // images on the left for (int infoID = mCurrentInfo - 1; infoID >= 0; infoID--) { ViewInfo curr = mViewInfo[infoID]; if (curr != null) { ViewInfo next = mViewInfo[infoID + 1]; - curr.setLeftPosition(next.getLeftPosition() - curr.getView().getMeasuredWidth()); - curr.layoutIn(mDrawArea, mCenterPosition); + curr.setLeftPosition( + next.getLeftPosition() - curr.getView().getMeasuredWidth()); + curr.layoutIn(mDrawArea, mCenterPosition, mScale); } } @@ -285,8 +356,9 @@ public class FilmStripView extends ViewGroup { ViewInfo curr = mViewInfo[infoID]; if (curr != null) { ViewInfo prev = mViewInfo[infoID - 1]; - curr.setLeftPosition(prev.getLeftPosition() + prev.getView().getMeasuredWidth()); - curr.layoutIn(mDrawArea, mCenterPosition); + curr.setLeftPosition( + prev.getLeftPosition() + prev.getView().getMeasuredWidth()); + curr.layoutIn(mDrawArea, mCenterPosition, mScale); } } @@ -306,24 +378,43 @@ public class FilmStripView extends ViewGroup { layoutChildren(); } - public void setDataAdapter( - DataAdapter adapter, ContentResolver resolver) { + public void setDataAdapter(DataAdapter adapter) { mDataAdapter = adapter; mDataAdapter.suggestSize(getMeasuredWidth(), getMeasuredHeight()); - mDataAdapter.setDataListener(this); - mDataAdapter.requestLoad(resolver); + mDataAdapter.setListener(new DataAdapter.Listener() { + @Override + public void onDataLoaded() { + reload(); + } + + @Override + public void onDataInserted(int dataID) { + } + + @Override + public void onDataRemoved(int dataID) { + } + }); } - private void getInfo(int infoID, int dataID) { - View v = mDataAdapter.getView(mContext, dataID); - if (v == null) return; - v.setPadding(H_PADDING, 0, H_PADDING, 0); - addView(v); - ViewInfo info = new ViewInfo(dataID, v); - mViewInfo[infoID] = info; + public boolean isInCameraFullscreen() { + return (isAnchoredTo(0) && mScale == 1f + && getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW); } - public void onDataChanged() { + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (isInCameraFullscreen()) return false; + return true; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mGestureRecognizer.onTouchEvent(ev); + return true; + } + + public void reload() { removeAllViews(); int dataNumber = mDataAdapter.getTotalNumber(); if (dataNumber == 0) return; @@ -333,89 +424,315 @@ public class FilmStripView extends ViewGroup { // previous data exists. if (mViewInfo[mCurrentInfo] != null) { currentLeft = mViewInfo[mCurrentInfo].getLeftPosition(); - currentData = mViewInfo[mCurrentInfo].getId(); + currentData = mViewInfo[mCurrentInfo].getID(); } - getInfo(mCurrentInfo, currentData); + mViewInfo[mCurrentInfo] = buildInfoFromData(currentData); mViewInfo[mCurrentInfo].setLeftPosition(currentLeft); + if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW + && currentLeft == 0) { + // we are in camera mode by default. + mGeometryAnimator.lockPosition(currentLeft); + } for (int i = 1; mCurrentInfo + i < BUFFER_SIZE || mCurrentInfo - i >= 0; i++) { int infoID = mCurrentInfo + i; if (infoID < BUFFER_SIZE && mViewInfo[infoID - 1] != null) { - getInfo(infoID, mViewInfo[infoID - 1].getId() + 1); + mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID - 1].getID() + 1); } infoID = mCurrentInfo - i; if (infoID >= 0 && mViewInfo[infoID + 1] != null) { - getInfo(infoID, mViewInfo[infoID + 1].getId() - 1); + mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID + 1].getID() - 1); } } layoutChildren(); } - private void movePositionTo(int position) { - mScroller.startScroll(mCenterPosition, 0, position - mCenterPosition, - 0, BACK_SCROLL_DURATION); - layoutChildren(); - } + // GeometryAnimator controls all the geometry animations. It passively + // tells the geometry information on demand. + private class GeometryAnimator implements + ValueAnimator.AnimatorUpdateListener, + Animator.AnimatorListener { + + private ValueAnimator mScaleAnimator; + private boolean mHasNewScale; + private float mNewScale; + + private Scroller mScroller; + private boolean mHasNewPosition; + private DecelerateInterpolator mDecelerateInterpolator; + + private boolean mCanStopScroll; + private boolean mCanStopScale; + + private boolean mIsPositionLocked; + private int mLockedPosition; + + private Runnable mPostAction; + + GeometryAnimator(Context context) { + mScroller = new Scroller(context); + mHasNewPosition = false; + mScaleAnimator = new ValueAnimator(); + mScaleAnimator.addUpdateListener(GeometryAnimator.this); + mScaleAnimator.addListener(GeometryAnimator.this); + mDecelerateInterpolator = new DecelerateInterpolator(); + mCanStopScroll = true; + mCanStopScale = true; + mHasNewScale = false; + } - public void goToFirst() { - movePositionTo(0); - } + boolean hasNewGeometry() { + mHasNewPosition = mScroller.computeScrollOffset(); + if (!mHasNewPosition) { + mCanStopScroll = true; + } + // If the position is locked, then we always return true to force + // the position value to use the locked value. + return (mHasNewPosition || mHasNewScale || mIsPositionLocked); + } - @Override - public boolean onTouchEvent(MotionEvent ev) { - return mGestureDetector.onTouchEvent(ev); - } + // Always call hasNewGeometry() before getting the new scale value. + float getNewScale() { + if (!mHasNewScale) return mScale; + mHasNewScale = false; + return mNewScale; + } + + // Always call hasNewGeometry() before getting the new position value. + int getNewPosition() { + if (mIsPositionLocked) return mLockedPosition; + if (!mHasNewPosition) return mCenterPosition; + return mScroller.getCurrX(); + } - private class MyGestureListener - extends GestureDetector.SimpleOnGestureListener { + void lockPosition(int pos) { + mIsPositionLocked = true; + mLockedPosition = pos; + } - @Override - public boolean onDoubleTap(MotionEvent e) { - float x = (float) e.getX(); - float y = (float) e.getY(); - for (int i = 0; i < BUFFER_SIZE; i++) { - if (mViewInfo[i] == null) continue; - View v = mViewInfo[i].getView(); - if (x >= v.getLeft() && x < v.getRight() - && y >= v.getTop() && y < v.getBottom()) { - Log.v(TAG, "l, r, t, b " + v.getLeft() + ',' + v.getRight() - + ',' + v.getTop() + ',' + v.getBottom()); - movePositionTo(mViewInfo[i].getCenterPosition()); - break; - } + void unlockPosition() { + if (mIsPositionLocked) { + // only when the position is previously locked we set the current + // position to make it consistent. + mCenterPosition = mLockedPosition; + mIsPositionLocked = false; } + } + + void fling(int velocityX, int minX, int maxX) { + if (!stopScroll() || mIsPositionLocked) return; + mScroller.fling(mCenterPosition, 0, velocityX, 0, minX, maxX, 0, 0); + } + + boolean stopScroll() { + if (!mCanStopScroll) return false; + mScroller.forceFinished(true); + mHasNewPosition = false; return true; } + boolean stopScale() { + if (!mCanStopScale) return false; + mScaleAnimator.cancel(); + mHasNewScale = false; + return true; + } + + void stop() { + stopScroll(); + stopScale(); + } + + void scrollTo(int position, int duration, boolean interruptible) { + if (!stopScroll() || mIsPositionLocked) return; + mCanStopScroll = interruptible; + stopScroll(); + mScroller.startScroll(mCenterPosition, 0, position - mCenterPosition, + 0, duration); + } + + void scrollTo(int position, int duration) { + scrollTo(position, duration, true); + } + + void scaleTo(float scale, int duration, boolean interruptible) { + if (!stopScale()) return; + mCanStopScale = interruptible; + mScaleAnimator.setDuration(duration); + mScaleAnimator.setFloatValues(mScale, scale); + mScaleAnimator.setInterpolator(mDecelerateInterpolator); + mScaleAnimator.start(); + mHasNewScale = true; + } + + void scaleTo(float scale, int duration) { + scaleTo(scale, duration, true); + } + + void setPostAction(Runnable act) { + mPostAction = act; + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mHasNewScale = true; + mNewScale = (Float) animation.getAnimatedValue(); + layoutChildren(); + } + + @Override + public void onAnimationStart(Animator anim) { + } + + @Override + public void onAnimationEnd(Animator anim) { + if (mPostAction != null) { + mPostAction.run(); + mPostAction = null; + } + mCanStopScale = true; + } + + @Override + public void onAnimationCancel(Animator anim) { + mPostAction = null; + } + + @Override + public void onAnimationRepeat(Animator anim) { + } + } + + private class MyGestureReceiver implements FilmStripGestureRecognizer.Listener { + // Indicating the current trend of scaling is up (>1) or down (<1). + private float mScaleTrend; + + @Override + public boolean onSingleTapUp(float x, float y) { + return false; + } + + @Override + public boolean onDoubleTap(float x, float y) { + return false; + } + @Override - public boolean onDown(MotionEvent ev) { - if (mIsScrolling) stopScroll(); + public boolean onDown(float x, float y) { + mGeometryAnimator.stop(); return true; } @Override - public boolean onScroll( - MotionEvent e1, MotionEvent e2, float dx, float dy) { - stopScroll(); - mCenterPosition += dx; + public boolean onScroll(float x, float y, float dx, float dy) { + int deltaX = (int) (dx / mScale); + if (deltaX > 0 && isInCameraFullscreen() ) { + mGeometryAnimator.unlockPosition(); + mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false); + } + + mCenterPosition += deltaX; + + // Vertical part. Promote or demote. + int scaledDeltaY = (int) (dy / mScale); + + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null) continue; + Rect hitRect = new Rect(); + View v = mViewInfo[i].getView(); + v.getHitRect(hitRect); + if (hitRect.contains((int) x, (int) y)) { + ImageData data = mDataAdapter.getImageData(mViewInfo[i].getID()); + if ((data.isActionSupported(ImageData.ACTION_DEMOTE) && dy > 0) + || (data.isActionSupported(ImageData.ACTION_PROMOTE) && dy < 0)) { + mViewInfo[i].setOffsetY(mViewInfo[i].getOffsetY() - dy); + } + break; + } + } + layoutChildren(); return true; } @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, - float velocityY) { + public boolean onFling(float velocityX, float velocityY) { + float scaledVelocityX = velocityX / mScale; + if (isInCameraFullscreen() && scaledVelocityX < 0) { + mGeometryAnimator.unlockPosition(); + mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false); + } ViewInfo info = mViewInfo[mCurrentInfo]; int w = getWidth(); if (info == null) return true; - mScroller.fling(mCenterPosition, 0, (int) -velocityX, (int) velocityY, + mGeometryAnimator.fling((int) -scaledVelocityX, // estimation of possible length on the left - info.getLeftPosition() - info.getId() * w * 2, + info.getLeftPosition() - info.getID() * w * 2, // estimation of possible length on the right info.getLeftPosition() - + (mDataAdapter.getTotalNumber() - info.getId()) * w * 2, - 0, 0); + + (mDataAdapter.getTotalNumber() - info.getID()) * w * 2); + layoutChildren(); + return true; + } + + @Override + public boolean onScaleBegin(float focusX, float focusY) { + if (isInCameraFullscreen()) return false; + mScaleTrend = 1f; + return true; + } + + @Override + public boolean onScale(float focusX, float focusY, float scale) { + if (isInCameraFullscreen()) return false; + + mScaleTrend = mScaleTrend * 0.5f + scale * 0.5f; + mScale *= scale; + if (mScale <= FILM_STRIP_SCALE) mScale = FILM_STRIP_SCALE; + if (mScale >= MAX_SCALE) mScale = MAX_SCALE; layoutChildren(); return true; } + + @Override + public void onScaleEnd() { + if (mScaleTrend >= 1f) { + if (mScale != 1f) { + mGeometryAnimator.scaleTo(1f, DURATION_GEOMETRY_ADJUST, false); + } + + if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) { + if (isAnchoredTo(0)) { + mGeometryAnimator.lockPosition(mViewInfo[mCurrentInfo].getCenterX()); + } else { + mGeometryAnimator.scrollTo( + mViewInfo[mCurrentInfo].getCenterX(), + DURATION_GEOMETRY_ADJUST, false); + mGeometryAnimator.setPostAction(mLockPositionRunnable); + } + } + } else { + // Scale down to film strip mode. + if (mScale == FILM_STRIP_SCALE) { + mGeometryAnimator.unlockPosition(); + return; + } + mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false); + mGeometryAnimator.setPostAction(mUnlockPositionRunnable); + } + } + + private Runnable mLockPositionRunnable = new Runnable() { + @Override + public void run() { + mGeometryAnimator.lockPosition(mViewInfo[mCurrentInfo].getCenterX()); + } + }; + + private Runnable mUnlockPositionRunnable = new Runnable() { + @Override + public void run() { + mGeometryAnimator.unlockPosition(); + } + }; } } -- cgit v1.2.3