Tab switcher animation

    Bug: 5123884

    first step towards animations between browser and tab switcher

Change-Id: I1d959d42d0036f3c4498972fcc8ad434fa7f4437
diff --git a/res/layout/anim_screen.xml b/res/layout/anim_screen.xml
new file mode 100644
index 0000000..399595e
--- /dev/null
+++ b/res/layout/anim_screen.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:focusable="false">
+    <ImageView
+        android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/toolbar_height" />
+    <ImageView
+        android:id="@+id/content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+</LinearLayout>
diff --git a/res/layout/nav_tab_view.xml b/res/layout/nav_tab_view.xml
index 763f9c0..31df29b 100644
--- a/res/layout/nav_tab_view.xml
+++ b/res/layout/nav_tab_view.xml
@@ -17,7 +17,7 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/main"
-    android:layout_width="match_parent"
+    android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:orientation="vertical"
     android:background="@drawable/nav_tab_bg"
@@ -52,10 +52,10 @@
     </LinearLayout>
     <ImageView
         android:id="@+id/tab_view"
+        android:src="@drawable/ic_stop_holo_dark"
         android:layout_width="@dimen/nav_tab_width"
         android:layout_height="@dimen/nav_tab_height"
         android:paddingLeft="2dip"
         android:paddingRight="2dip"
-        android:layout_gravity="center_horizontal"
         android:focusable="false" />
 </LinearLayout>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index d6b74bd..f342d4c 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -19,4 +19,5 @@
     <item type="id" name="group_position" />
     <item type="id" name="child_position" />
     <item type="id" name="child_id" />
+    <item type="id" name="tab_view" />
 </resources>
diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java
index 6ec6071..084ace0 100644
--- a/src/com/android/browser/BrowserActivity.java
+++ b/src/com/android/browser/BrowserActivity.java
@@ -28,6 +28,7 @@
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.Window;
 import android.view.WindowManager;
@@ -271,4 +272,34 @@
         return mController.onSearchRequested();
     }
 
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        return mController.dispatchKeyEvent(event)
+                || super.dispatchKeyEvent(event);
+    }
+
+    @Override
+    public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+        return mController.dispatchKeyShortcutEvent(event)
+                || super.dispatchKeyShortcutEvent(event);
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        return mController.dispatchTouchEvent(ev)
+                || super.dispatchTouchEvent(ev);
+    }
+
+    @Override
+    public boolean dispatchTrackballEvent(MotionEvent ev) {
+        return mController.dispatchTrackballEvent(ev)
+                || super.dispatchTrackballEvent(ev);
+    }
+
+    @Override
+    public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+        return mController.dispatchGenericMotionEvent(ev) ||
+                super.dispatchGenericMotionEvent(ev);
+    }
+
 }
diff --git a/src/com/android/browser/Controller.java b/src/com/android/browser/Controller.java
index 2144dd0..3f126c0 100644
--- a/src/com/android/browser/Controller.java
+++ b/src/com/android/browser/Controller.java
@@ -64,6 +64,7 @@
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.MotionEvent;
 import android.view.View;
 import android.webkit.CookieManager;
 import android.webkit.CookieSyncManager;
@@ -212,6 +213,8 @@
 
     private boolean mSimulateActionBarOverlayMode;
 
+    private boolean mBlockEvents;
+
     private static class ClearThumbnails extends AsyncTask<File, Void, Void> {
         @Override
         public Void doInBackground(File... files) {
@@ -2675,4 +2678,29 @@
         return mUi.shouldCaptureThumbnails();
     }
 
+    @Override
+    public void setBlockEvents(boolean block) {
+        mBlockEvents = block;
+    }
+
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        return mBlockEvents;
+    }
+
+    public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+        return mBlockEvents;
+    }
+
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        return mBlockEvents;
+    }
+
+    public boolean dispatchTrackballEvent(MotionEvent ev) {
+        return mBlockEvents;
+    }
+
+    public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+        return mBlockEvents;
+    }
+
 }
diff --git a/src/com/android/browser/NavScreen.java b/src/com/android/browser/NavScreen.java
index 22f6257..768f9ba 100644
--- a/src/com/android/browser/NavScreen.java
+++ b/src/com/android/browser/NavScreen.java
@@ -39,12 +39,17 @@
 
 import com.android.browser.NavTabGallery.OnRemoveListener;
 import com.android.browser.TabControl.OnThumbnailUpdatedListener;
+import com.android.browser.view.Gallery.OnScrollFinishedListener;
 
 import java.util.HashMap;
 
 public class NavScreen extends RelativeLayout
         implements OnClickListener, OnMenuItemClickListener, OnThumbnailUpdatedListener {
 
+
+    private static final int SCROLL_MIN = 200;
+    private static final int SCROLL_FACTOR = 20;
+
     UiController mUiController;
     PhoneUi mUi;
     Tab mTab;
@@ -126,11 +131,11 @@
         TabControl tc = mUiController.getTabControl();
         mTabViews = new HashMap<Tab, View>(tc.getTabCount());
         mAdapter = new TabAdapter(mContext, tc);
-        mScroller.setAdapter(mAdapter);
         mScroller.setOrientation(mOrientation == Configuration.ORIENTATION_LANDSCAPE
                 ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
         // update state for active tab
-        mScroller.setSelection(mUiController.getTabControl().getTabPosition(mUi.getActiveTab()));
+        mScroller.setAdapter(mAdapter,
+                mUiController.getTabControl().getTabPosition(mUi.getActiveTab()));
         mScroller.setOnRemoveListener(new OnRemoveListener() {
             public void onRemovePosition(int pos) {
                 Tab tab = mAdapter.getItem(pos);
@@ -185,21 +190,28 @@
         // need to call openTab explicitely with setactive false
         Tab tab = mUiController.openTab(BrowserSettings.getInstance().getHomePage(),
                 false, false, false);
-        mAdapter.notifyDataSetChanged();
+        int duration = 0;
         if (tab != null) {
-            // set tab as the selected in flipper, then hide
+            mUiController.setBlockEvents(true);
+            int oldsel = mScroller.getSelectedItemPosition();
             final int tix = mUi.mTabControl.getTabPosition(tab);
-            mScroller.setSelection(tix);
-            postDelayed(new Runnable() {
+            duration = SCROLL_MIN + SCROLL_FACTOR * Math.abs(oldsel - tix);
+            mScroller.handleDataChanged();
+            mScroller.smoothScrollToPosition(tix, duration, new OnScrollFinishedListener() {
                 @Override
-                public void run() {
+                public void onScrollFinished() {
+                    mUiController.setBlockEvents(false);
                     mUi.hideNavScreen(true);
                     switchToSelected();
                 }
-            }, 100);
+            });
         }
     }
 
+    View getSelectedTabView() {
+        return mScroller.getSelectedTab();
+    }
+
     private void switchToSelected() {
         Tab tab = (Tab) mScroller.getSelectedItem();
         if (tab != mUi.getActiveTab()) {
@@ -243,7 +255,7 @@
         public View getView(final int position, View convertView, ViewGroup parent) {
             final NavTabView tabview = new NavTabView(mActivity);
             final Tab tab = getItem(position);
-            tabview.setWebView(mUi, tab);
+            tabview.setWebView(tab);
             mTabViews.put(tab, tabview.mImage);
             tabview.setOnClickListener(new OnClickListener() {
                 @Override
diff --git a/src/com/android/browser/NavTabView.java b/src/com/android/browser/NavTabView.java
index ed6b63d..07ac164 100644
--- a/src/com/android/browser/NavTabView.java
+++ b/src/com/android/browser/NavTabView.java
@@ -21,12 +21,15 @@
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 public class NavTabView extends LinearLayout {
 
+    private ViewGroup mContent;
     private Tab mTab;
     private ImageView mClose;
     private TextView mTitle;
@@ -52,6 +55,7 @@
 
     private void init() {
         LayoutInflater.from(mContext).inflate(R.layout.nav_tab_view, this);
+        mContent = (ViewGroup) findViewById(R.id.main);
         mClose = (ImageView) findViewById(R.id.closetab);
         mTitle = (TextView) findViewById(R.id.title);
         mTitleBar = findViewById(R.id.titlebar);
@@ -92,7 +96,11 @@
         return mHighlighted;
     }
 
-    protected void setWebView(PhoneUi ui, Tab tab) {
+    protected void setWebView(WebView w) {
+        mContent.addView(w, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+    }
+
+    protected void setWebView(Tab tab) {
         mTab = tab;
         setTitle();
         Bitmap image = tab.getScreenshot();
diff --git a/src/com/android/browser/PhoneUi.java b/src/com/android/browser/PhoneUi.java
index 863a628..d77fcdb 100644
--- a/src/com/android/browser/PhoneUi.java
+++ b/src/com/android/browser/PhoneUi.java
@@ -16,17 +16,29 @@
 
 package com.android.browser;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
 import android.util.Log;
 import android.view.ActionMode;
 import android.view.Gravity;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.DecelerateInterpolator;
 import android.webkit.WebView;
 import android.widget.FrameLayout;
+import android.widget.ImageView;
 
 import com.android.browser.UrlInputView.StateListener;
 
@@ -241,15 +253,72 @@
         updateUrlBarAutoShowManagerTarget();
     }
 
+    @Override
+    public boolean isWebShowing() {
+        return super.isWebShowing() && mNavScreen == null;
+    }
+
+    @Override
+    public void showWeb(boolean animate) {
+        super.showWeb(animate);
+        hideNavScreen(animate);
+    }
+
     void showNavScreen() {
-        mActiveTab.capture();
-        detachTab(mActiveTab);
+        mUiController.setBlockEvents(true);
         mNavScreen = new NavScreen(mActivity, mUiController, this);
-        // Add the custom view to its container.
+        mActiveTab.capture();
+        // Add the custom view to its container
         mCustomViewContainer.addView(mNavScreen, COVER_SCREEN_PARAMS);
-        mContentView.setVisibility(View.GONE);
+        AnimScreen ascreen = new AnimScreen(mActivity, getTitleBar(), getWebView());
+        final View animView = ascreen.mMain;
+        mCustomViewContainer.addView(animView, COVER_SCREEN_PARAMS);
         mCustomViewContainer.setVisibility(View.VISIBLE);
         mCustomViewContainer.bringToFront();
+        View target = ((NavTabView) mNavScreen.mScroller.getSelectedView()).mImage;
+        int fromLeft = 0;
+        int fromTop = getTitleBar().getHeight();
+        int fromRight = mContentView.getWidth();
+        int fromBottom = mContentView.getHeight();
+        int width = target.getWidth();
+        int height = target.getHeight();
+        int toLeft = (mContentView.getWidth() - width) / 2;
+        int toTop = fromTop + (mContentView.getHeight() - fromTop - height) / 2;
+        int toRight = toLeft + width;
+        int toBottom = toTop + height;
+        float scaleFactor = width / (float) mContentView.getWidth();
+        detachTab(mActiveTab);
+        mContentView.setVisibility(View.GONE);
+        AnimatorSet inanim = new AnimatorSet();
+        ObjectAnimator tx = ObjectAnimator.ofInt(ascreen.mContent, "left",
+                fromLeft, toLeft);
+        ObjectAnimator ty = ObjectAnimator.ofInt(ascreen.mContent, "top",
+                fromTop, toTop);
+        ObjectAnimator tr = ObjectAnimator.ofInt(ascreen.mContent, "right",
+                fromRight, toRight);
+        ObjectAnimator tb = ObjectAnimator.ofInt(ascreen.mContent, "bottom",
+                fromBottom, toBottom);
+        ObjectAnimator title = ObjectAnimator.ofFloat(ascreen.mTitle, "alpha",
+                1f, 0f);
+        ObjectAnimator content = ObjectAnimator.ofFloat(ascreen.mContent, "alpha",
+                1f, 0f);
+        ObjectAnimator sx = ObjectAnimator.ofFloat(ascreen, "scaleFactor",
+                1f, scaleFactor);
+        inanim.playTogether(tx, ty, tr, tb, title, content, sx);
+        inanim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator anim) {
+                mCustomViewContainer.removeView(animView);
+                finishAnimationIn();
+                mUiController.setBlockEvents(false);
+            }
+        });
+        inanim.setInterpolator(new DecelerateInterpolator(2f));
+        inanim.setDuration(300);
+        inanim.start();
+    }
+
+    private void finishAnimationIn() {
         // notify accessibility manager about the screen change
         mNavScreen.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
         mTabControl.setOnThumbnailUpdatedListener(mNavScreen);
@@ -257,14 +326,67 @@
 
     void hideNavScreen(boolean animate) {
         if (mNavScreen == null) return;
+        final Tab tab = mNavScreen.getSelectedTab();
+        if ((tab != null) && !animate) {
+            finishAnimateOut(tab);
+        }
+        NavTabView tabview = (NavTabView) mNavScreen.getSelectedTabView();
+        if (tabview == null) {
+            finishAnimateOut(tab);
+        }
+        mUiController.setBlockEvents(true);
+        mUiController.setActiveTab(tab);
+        mContentView.setVisibility(View.VISIBLE);
+        final AnimScreen screen = new AnimScreen(mActivity, tab.getScreenshot());
+        View target = ((NavTabView) mNavScreen.mScroller.getSelectedView()).mImage;
+        int toLeft = 0;
+        int toTop = getTitleBar().getHeight();
+        int toRight = mContentView.getWidth();
+        int width = target.getWidth();
+        int height = target.getHeight();
+        int[] pos = new int[2];
+        tabview.mImage.getLocationInWindow(pos);
+        int fromLeft = pos[0];
+        int fromTop = pos[1];
+        int fromRight = fromLeft + width;
+        int fromBottom = fromTop + height;
+        float scaleFactor = mContentView.getWidth() / (float) width;
+        int toBottom = (int) (height * scaleFactor);
+        screen.mMain.setAlpha(0f);
+        mCustomViewContainer.addView(screen.mMain, COVER_SCREEN_PARAMS);
+        AnimatorSet animSet = new AnimatorSet();
+        ObjectAnimator l = ObjectAnimator.ofInt(screen.mContent, "left",
+                fromLeft, toLeft);
+        ObjectAnimator t = ObjectAnimator.ofInt(screen.mContent, "top",
+                fromTop, toTop);
+        ObjectAnimator r = ObjectAnimator.ofInt(screen.mContent, "right",
+                fromRight, toRight);
+        ObjectAnimator b = ObjectAnimator.ofInt(screen.mContent, "bottom",
+                fromBottom, toBottom);
+        ObjectAnimator scale = ObjectAnimator.ofFloat(screen, "scaleFactor",
+                1f, scaleFactor);
+        ObjectAnimator alpha = ObjectAnimator.ofFloat(screen.mMain, "alpha", 1f, 1f);
+        ObjectAnimator otheralpha = ObjectAnimator.ofFloat(mCustomViewContainer, "alpha", 1f, 0f);
+        alpha.setStartDelay(100);
+        animSet.playTogether(l, t, r, b, scale, alpha, otheralpha);
+        animSet.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator anim) {
+                mCustomViewContainer.removeView(screen.mMain);
+                finishAnimateOut(tab);
+                mUiController.setBlockEvents(false);
+            }
+        });
+        animSet.setDuration(250);
+        animSet.start();
+    }
+
+    private void finishAnimateOut(Tab tab) {
         mTabControl.setOnThumbnailUpdatedListener(null);
-        Tab tab = mNavScreen.getSelectedTab();
         mCustomViewContainer.removeView(mNavScreen);
+        mCustomViewContainer.setAlpha(1f);
         mNavScreen = null;
         mCustomViewContainer.setVisibility(View.GONE);
-        mUiController.setActiveTab(tab);
-        // Show the content view.
-        mContentView.setVisibility(View.VISIBLE);
     }
 
     @Override
@@ -285,15 +407,62 @@
         return true;
     }
 
-    @Override
-    public boolean isWebShowing() {
-        return super.isWebShowing() && mNavScreen == null;
-    }
+    static class AnimScreen {
 
-    @Override
-    public void showWeb(boolean animate) {
-        super.showWeb(animate);
-        hideNavScreen(animate);
+        private View mMain;
+        private ImageView mTitle;
+        private ImageView mContent;
+        private float mScale;
+
+        public AnimScreen(Context ctx, TitleBar tbar, WebView web) {
+            mMain = LayoutInflater.from(ctx).inflate(R.layout.anim_screen,
+                    null);
+            mContent = (ImageView) mMain.findViewById(R.id.content);
+            mContent.setTop(tbar.getHeight());
+
+            mTitle = (ImageView) mMain.findViewById(R.id.title);
+            Bitmap bm1 = Bitmap.createBitmap(tbar.getWidth(), tbar.getHeight(),
+                    Bitmap.Config.RGB_565);
+            Canvas c1 = new Canvas(bm1);
+            tbar.draw(c1);
+            mTitle.setImageBitmap(bm1);
+            int h = web.getHeight() - tbar.getHeight();
+            Bitmap bm2 = Bitmap.createBitmap(web.getWidth(), h,
+                    Bitmap.Config.RGB_565);
+            Canvas c2 = new Canvas(bm2);
+            int tx = web.getScrollX();
+            int ty = web.getScrollY();
+            c2.translate(-tx, -ty - tbar.getHeight());
+            web.draw(c2);
+            mContent.setImageBitmap(bm2);
+            mContent.setScaleType(ImageView.ScaleType.MATRIX);
+            mContent.setImageMatrix(new Matrix());
+            mScale = 1.0f;
+            setScaleFactor(getScaleFactor());
+        }
+
+        public AnimScreen(Context ctx, Bitmap image) {
+            mMain = LayoutInflater.from(ctx).inflate(R.layout.anim_screen,
+                    null);
+            mContent = (ImageView) mMain.findViewById(R.id.content);
+            mContent.setImageBitmap(image);
+            mContent.setScaleType(ImageView.ScaleType.MATRIX);
+            mContent.setImageMatrix(new Matrix());
+            mScale = 1.0f;
+            setScaleFactor(getScaleFactor());
+        }
+
+        public void setScaleFactor(float sf) {
+            mScale = sf;
+            Matrix m = new Matrix();
+            m.postScale(sf,sf);
+            mContent.setImageMatrix(m);
+        }
+
+        public float getScaleFactor() {
+            return mScale;
+        }
+
     }
 
 }
diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java
index 78ae987..c6808e0 100644
--- a/src/com/android/browser/Tab.java
+++ b/src/com/android/browser/Tab.java
@@ -31,6 +31,7 @@
 import android.graphics.Bitmap.CompressFormat;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Picture;
 import android.net.Uri;
 import android.net.http.SslError;
@@ -1455,6 +1456,7 @@
                 if (mCapture == null) {
                     mCapture = Bitmap.createBitmap(mCaptureWidth, mCaptureHeight,
                             Bitmap.Config.RGB_565);
+                    mCapture.eraseColor(Color.WHITE);
                     if (mInForeground) {
                         postCapture();
                     }
diff --git a/src/com/android/browser/UiController.java b/src/com/android/browser/UiController.java
index 14d498c..0da523a 100644
--- a/src/com/android/browser/UiController.java
+++ b/src/com/android/browser/UiController.java
@@ -98,4 +98,6 @@
 
     void loadUrl(Tab tab, String url);
 
+    void setBlockEvents(boolean block);
+
 }
diff --git a/src/com/android/browser/view/Gallery.java b/src/com/android/browser/view/Gallery.java
index 78d4bc6..2e2c75f 100644
--- a/src/com/android/browser/view/Gallery.java
+++ b/src/com/android/browser/view/Gallery.java
@@ -34,6 +34,8 @@
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
+import android.view.animation.BounceInterpolator;
+import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Transformation;
 import android.widget.BaseAdapter;
 import android.widget.LinearLayout;
@@ -130,6 +132,8 @@
     private float mLastMotionCoord;
     private float mLastOrthoCoord;
 
+    private int mScrollValue;
+
     public Gallery(Context context) {
         this(context, null);
     }
@@ -172,6 +176,7 @@
         mGap = 0;
         // proguard
         setGap(getGap());
+        setScrollValue(getScrollValue());
     }
 
     /**
@@ -180,7 +185,10 @@
      */
     public interface OnItemSelectedListener {
         void onItemSelected(ViewGroup parent, View view, int position, long id);
+    }
 
+    public interface OnScrollFinishedListener {
+        void onScrollFinished();
     }
 
     /**
@@ -226,6 +234,11 @@
         return mGap;
     }
 
+    public void setAdapter(BaseAdapter adapter, int selpos) {
+        mSelectedPosition = selpos;
+        setAdapter(adapter);
+    }
+
     public void setAdapter(BaseAdapter adapter) {
         mAdapter = adapter;
         if (mAdapter != null) {
@@ -246,7 +259,7 @@
         handleDataChanged();
     }
 
-    void handleDataChanged() {
+    public void handleDataChanged() {
         if (mAdapter != null) {
             if (mGapAnimator != null) {
                 mGapAnimator.cancel();
@@ -427,7 +440,8 @@
             return;
         }
         boolean toLeft = deltaX < 0;
-        int limitedDeltaX = getLimitedMotionScrollAmount(toLeft, deltaX);
+        int limitedDeltaX = mFlingRunnable.mScroller.isFinished()
+                ? deltaX : getLimitedMotionScrollAmount(toLeft, deltaX);
         if (limitedDeltaX != deltaX) {
             // The above call returned a limited amount, so stop any
             // scrolls/flings
@@ -1178,7 +1192,7 @@
         }
     }
 
-    boolean moveNext() {
+    public boolean moveNext() {
         if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
             scrollToChild(mSelectedPosition - mFirstPosition + 1);
             return true;
@@ -1187,16 +1201,48 @@
         }
     }
 
-    protected boolean scrollToChild(int childPosition) {
+    public boolean scrollToChild(int childPosition) {
         View child = getChildAt(childPosition);
         if (child != null) {
             int distance = getCenterOfGallery() - getCenterOfView(child);
-            mFlingRunnable.startUsingDistance(distance);
+            mFlingRunnable.startUsingDistance(distance, 0);
             return true;
         }
         return false;
     }
 
+    /**
+     * use the scroller to scroll to a new position, independent
+     * of whether attached or not
+     * this uses trackMotionScroll, which will set the selection
+     */
+    public void smoothScrollToPosition(int pos, int duration,
+            final OnScrollFinishedListener listener) {
+        if (pos >= mAdapter.getCount() || getChildCount() < 1) return;
+        int dist = (mSelectedPosition - pos) * (mHorizontal ? getChildHeight(getChildAt(0))
+                : getChildWidth(getChildAt(0)));
+        ObjectAnimator scroll = ObjectAnimator.ofInt(this, "scrollValue", 0, dist);
+        scroll.setDuration(duration);
+        scroll.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mScrollValue = 0;
+                listener.onScrollFinished();
+            }
+        });
+        scroll.setInterpolator(new BounceInterpolator());
+        scroll.start();
+    }
+
+    public void setScrollValue(int scroll) {
+        trackMotionScroll(scroll - mScrollValue);
+        mScrollValue = scroll;
+    }
+
+    public int getScrollValue() {
+        return mScrollValue;
+    }
+
     protected void setSelectedPositionInt(int position) {
         mSelectedPosition = position;
         updateSelectedItemMetadata();
@@ -1368,11 +1414,15 @@
         }
 
         public void startUsingDistance(int distance) {
+            startUsingDistance(distance, mAnimationDuration);
+        }
+
+        public void startUsingDistance(int distance, int duration) {
             if (distance == 0)
                 return;
             startCommon();
             mLastFlingX = 0;
-            mScroller.startScroll(0, 0, -distance, 0, mAnimationDuration);
+            mScroller.startScroll(0, 0, -distance, 0, duration);
             post(this);
         }