blob: 0a19effb29bb60ba0251433f16d7479d73178e66 [file] [log] [blame]
/*
* 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.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
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.class.getSimpleName();
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 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 FilmStripGestureRecognizer mGestureRecognizer;
private DataAdapter mDataAdapter;
private final Rect mDrawArea = new Rect();
private int mCurrentInfo;
private float mScale;
private GeometryAnimator mGeometryAnimator;
private int mCenterPosition = -1;
private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE];
// This is used to resolve the misalignment problem when the device
// orientation is changed. If the current item is in fullscreen, it might
// be shifted because mCenterPosition is not adjusted with the orientation.
// Set this to true when onSizeChanged is called to make sure we adjust
// mCenterPosition accordingly.
private boolean mAnchorPending;
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 StatusReporter {
public boolean isDataRemoved(int id);
public boolean isDataUpdated(int id);
}
public interface Listener {
// Called when the whole data loading is done. No any assumption
// on previous data.
public void onDataLoaded();
// Only some of the data is changed. The listener should check
// if any thing needs to be updated.
public void onDataUpdated(StatusReporter reporter);
public void onDataInserted(int dataID);
public void onDataRemoved(int dataID);
}
public int getTotalNumber();
public View getView(Context context, int id);
public ImageData getImageData(int id);
public void suggestSize(int w, int h);
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 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() {
return mDataID;
}
public void setLeftPosition(int pos) {
mLeftPosition = pos;
}
public int getLeftPosition() {
return mLeftPosition;
}
public float getOffsetY() {
return mOffsetY;
}
public void setOffsetY(float offset) {
mOffsetY = offset;
}
public int getCenterX() {
return mLeftPosition + mView.getWidth() / 2;
}
public View getView() {
return mView;
}
private void layoutAt(int left, int top) {
mView.layout(left, top, left + mView.getMeasuredWidth(),
top + mView.getMeasuredHeight());
}
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.
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);
}
}
public FilmStripView(Context context) {
super(context);
init(context);
}
public FilmStripView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public FilmStripView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
mCurrentInfo = (BUFFER_SIZE - 1) / 2;
// This is for positioning camera controller at the same place in
// different orientations.
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
setWillNotDraw(false);
mContext = context;
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() {
if (mDataAdapter == null) return ImageData.TYPE_NONE;
ViewInfo curr = mViewInfo[mCurrentInfo];
if (curr == null) return ImageData.TYPE_NONE;
return mDataAdapter.getImageData(curr.getID()).getType();
}
@Override
public void onDraw(Canvas c) {
if (mGeometryAnimator.hasNewGeometry()) {
layoutChildren();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
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;
for (int i = 0; i < mViewInfo.length; i++) {
ViewInfo info = mViewInfo[i];
if (mViewInfo[i] == null) continue;
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;
mViewInfo[i].getView().measure(
View.MeasureSpec.makeMeasureSpec(scaledWidth, wMode)
, View.MeasureSpec.makeMeasureSpec(scaledHeight, hMode));
}
setMeasuredDimension(boundWidth, boundHeight);
}
private int findTheNearestView(int pointX) {
int nearest = 0;
// find the first non-null ViewInfo.
for (; nearest < BUFFER_SIZE
&& (mViewInfo[nearest] == null || mViewInfo[nearest].getLeftPosition() == -1);
nearest++);
// no existing available ViewInfo
if (nearest == BUFFER_SIZE) return -1;
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].getCenterX();
int dist = Math.abs(pointX - c);
if (dist < min) {
min = dist;
nearest = infoID;
}
}
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);
ViewInfo info = new ViewInfo(dataID, v);
addView(info.getView());
return info;
}
// We try to keep the one closest to the center of the screen at position mCurrentInfo.
private void stepIfNeeded() {
int nearest = findTheNearestView(mCenterPosition);
// no change made.
if (nearest == -1 || nearest == mCurrentInfo) return;
int adjust = nearest - mCurrentInfo;
if (adjust > 0) {
for (int k = 0; k < adjust; k++) {
if (mViewInfo[k] != null) {
removeView(mViewInfo[k].getView());
}
}
for (int k = 0; k + adjust < BUFFER_SIZE; k++) {
mViewInfo[k] = mViewInfo[k + adjust];
}
for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) {
mViewInfo[k] = null;
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--) {
if (mViewInfo[k] != null) {
removeView(mViewInfo[k].getView());
}
}
for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) {
mViewInfo[k] = mViewInfo[k + adjust];
}
for (int k = -1 - adjust; k >= 0; k--) {
mViewInfo[k] = null;
if (mViewInfo[k + 1] != null)
mViewInfo[k] = buildInfoFromData(mViewInfo[k + 1].getID() - 1);
}
}
}
// Don't go out of bound.
private void adjustCenterPosition() {
ViewInfo curr = mViewInfo[mCurrentInfo];
if (curr == null) return;
if (curr.getID() == 0 && mCenterPosition < curr.getCenterX()) {
mCenterPosition = curr.getCenterX();
mGeometryAnimator.stopScroll();
}
if (curr.getID() == mDataAdapter.getTotalNumber() - 1
&& mCenterPosition > curr.getCenterX()) {
mCenterPosition = curr.getCenterX();
mGeometryAnimator.stopScroll();
}
}
private void layoutChildren() {
if (mAnchorPending) {
mCenterPosition = mViewInfo[mCurrentInfo].getCenterX();
mAnchorPending = false;
}
if (mGeometryAnimator.hasNewGeometry()) {
mCenterPosition = mGeometryAnimator.getNewPosition();
mScale = mGeometryAnimator.getNewScale();
}
adjustCenterPosition();
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, mScale);
}
}
// images on the right
for (int infoID = mCurrentInfo + 1; infoID < BUFFER_SIZE; infoID++) {
ViewInfo curr = mViewInfo[infoID];
if (curr != null) {
ViewInfo prev = mViewInfo[infoID - 1];
curr.setLeftPosition(
prev.getLeftPosition() + prev.getView().getMeasuredWidth());
curr.layoutIn(mDrawArea, mCenterPosition, mScale);
}
}
stepIfNeeded();
invalidate();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mViewInfo[mCurrentInfo] == null) return;
mDrawArea.left = l;
mDrawArea.top = t;
mDrawArea.right = r;
mDrawArea.bottom = b;
layoutChildren();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w == oldw && h == oldh) return;
if (mViewInfo[mCurrentInfo] != null && mScale == 1f
&& isAnchoredTo(mViewInfo[mCurrentInfo].getID())) {
mAnchorPending = true;
}
}
public void setDataAdapter(DataAdapter adapter) {
mDataAdapter = adapter;
mDataAdapter.suggestSize(getMeasuredWidth(), getMeasuredHeight());
mDataAdapter.setListener(new DataAdapter.Listener() {
@Override
public void onDataLoaded() {
reload();
}
@Override
public void onDataUpdated(DataAdapter.StatusReporter reporter) {
update(reporter);
}
@Override
public void onDataInserted(int dataID) {
}
@Override
public void onDataRemoved(int dataID) {
}
});
}
public boolean isInCameraFullscreen() {
return (isAnchoredTo(0) && mScale == 1f
&& getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (isInCameraFullscreen()) return false;
return true;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
mGestureRecognizer.onTouchEvent(ev);
return true;
}
private void updateViewInfo(int infoID) {
ViewInfo info = mViewInfo[infoID];
removeView(info.getView());
mViewInfo[infoID] = buildInfoFromData(info.getID());
}
// Some of the data is changed.
private void update(DataAdapter.StatusReporter reporter) {
// No data yet.
if (mViewInfo[mCurrentInfo] == null) {
reload();
return;
}
// Check the current one.
ViewInfo curr = mViewInfo[mCurrentInfo];
int dataID = curr.getID();
if (reporter.isDataRemoved(dataID)) {
mCenterPosition = -1;
reload();
return;
}
if (reporter.isDataUpdated(dataID)) {
updateViewInfo(mCurrentInfo);
}
// Check left
for (int i = mCurrentInfo - 1; i >= 0; i--) {
curr = mViewInfo[i];
if (curr != null) {
dataID = curr.getID();
if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) {
updateViewInfo(i);
}
} else {
ViewInfo next = mViewInfo[i + 1];
if (next != null) mViewInfo[i] = buildInfoFromData(next.getID() - 1);
}
}
// Check right
for (int i = mCurrentInfo + 1; i < BUFFER_SIZE; i++) {
curr = mViewInfo[i];
if (curr != null) {
dataID = curr.getID();
if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) {
updateViewInfo(i);
}
} else {
ViewInfo prev = mViewInfo[i - 1];
if (prev != null) mViewInfo[i] = buildInfoFromData(prev.getID() + 1);
}
}
}
// The whole data might be totally different. Flush all and load from the start.
private void reload() {
removeAllViews();
int dataNumber = mDataAdapter.getTotalNumber();
if (dataNumber == 0) return;
int currentData = 0;
int currentLeft = 0;
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) {
mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID - 1].getID() + 1);
}
infoID = mCurrentInfo - i;
if (infoID >= 0 && mViewInfo[infoID + 1] != null) {
mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID + 1].getID() - 1);
}
}
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;
}
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);
}
// 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();
}
void lockPosition(int pos) {
mIsPositionLocked = true;
mLockedPosition = pos;
}
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(float x, float y) {
mGeometryAnimator.stop();
return true;
}
@Override
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(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;
mGeometryAnimator.fling((int) -scaledVelocityX,
// estimation of possible length on the left
info.getLeftPosition() - info.getID() * w * 2,
// estimation of possible length on the right
info.getLeftPosition()
+ (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();
}
};
}
}