RecentsVerticalScrollView.java revision 9f0f0e0e3100caec459a5b5ef836317844c83b3f
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
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
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17package com.android.systemui.recent;
18
19import com.android.systemui.recent.RecentsPanelView.ActvityDescriptionAdapter;
20
21import android.animation.Animator;
22import android.animation.Animator.AnimatorListener;
23import android.animation.LayoutTransition;
24import android.animation.ObjectAnimator;
25import android.animation.ValueAnimator;
26import android.animation.ValueAnimator.AnimatorUpdateListener;
27import android.content.Context;
28import android.database.DataSetObserver;
29import android.graphics.RectF;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.view.LayoutInflater;
33import android.view.MotionEvent;
34import android.view.VelocityTracker;
35import android.view.View;
36import android.view.animation.DecelerateInterpolator;
37import android.view.animation.LinearInterpolator;
38import android.widget.LinearLayout;
39import android.widget.ScrollView;
40
41import com.android.systemui.R;
42
43public class RecentsVerticalScrollView extends ScrollView
44        implements View.OnClickListener, View.OnTouchListener {
45    private static final float FADE_CONSTANT = 0.5f;
46    private static final int SNAP_BACK_DURATION = 250;
47    private static final int ESCAPE_VELOCITY = 100; // speed of item required to "curate" it
48    private static final String TAG = RecentsPanelView.TAG;
49    private static final float THRESHHOLD = 50;
50    private static final boolean DEBUG_INVALIDATE = false;
51    private LinearLayout mLinearLayout;
52    private ActvityDescriptionAdapter mAdapter;
53    private RecentsCallback mCallback;
54    protected int mLastScrollPosition;
55    private View mCurrentView;
56    private float mLastX;
57    private boolean mDragging;
58    private VelocityTracker mVelocityTracker;
59
60    public RecentsVerticalScrollView(Context context) {
61        this(context, null);
62    }
63
64    public RecentsVerticalScrollView(Context context, AttributeSet attrs) {
65        super(context, attrs, 0);
66    }
67
68    private int scrollPositionOfMostRecent() {
69        return mLinearLayout.getHeight() - getHeight();
70    }
71
72    public void update() {
73        mLinearLayout.removeAllViews();
74        for (int i = 0; i < mAdapter.getCount(); i++) {
75            View view = mAdapter.getView(i, null, mLinearLayout);
76            view.setClickable(true);
77            view.setOnClickListener(this);
78            view.setOnTouchListener(this);
79            mLinearLayout.addView(view);
80        }
81        // Scroll to end after layout.
82        post(new Runnable() {
83            public void run() {
84                mLastScrollPosition = scrollPositionOfMostRecent();
85                scrollTo(0, mLastScrollPosition);
86            }
87        });
88    }
89
90    @Override
91    public boolean onInterceptTouchEvent(MotionEvent ev) {
92        if (mVelocityTracker == null) {
93            mVelocityTracker = VelocityTracker.obtain();
94        }
95        mVelocityTracker.addMovement(ev);
96        switch (ev.getAction()) {
97            case MotionEvent.ACTION_DOWN:
98                mDragging = false;
99                mLastX = ev.getX();
100                break;
101
102            case MotionEvent.ACTION_MOVE:
103                float delta = ev.getX() - mLastX;
104                Log.v(TAG, "ACTION_MOVE : " + delta);
105                if (Math.abs(delta) > THRESHHOLD) {
106                    mDragging = true;
107                }
108                break;
109
110            case MotionEvent.ACTION_UP:
111                mDragging = false;
112                break;
113        }
114        return mDragging ? true : super.onInterceptTouchEvent(ev);
115    }
116
117    private float getAlphaForOffset(View view, float thumbWidth) {
118        final float fadeWidth = FADE_CONSTANT * thumbWidth;
119        float result = 1.0f;
120        if (view.getX() >= thumbWidth) {
121            result = 1.0f - (view.getX() - thumbWidth) / fadeWidth;
122        } else if (view.getX() < 0.0f) {
123            result = 1.0f + (thumbWidth + view.getX()) / fadeWidth;
124        }
125        Log.v(TAG, "FADE AMOUNT: " + result);
126        return result;
127    }
128
129    @Override
130    public boolean onTouchEvent(MotionEvent ev) {
131        if (!mDragging) {
132            return super.onTouchEvent(ev);
133        }
134
135        mVelocityTracker.addMovement(ev);
136
137        final View animView = mCurrentView;
138        // TODO: Cache thumbnail
139        final View thumb = animView.findViewById(R.id.app_thumbnail);
140        switch (ev.getAction()) {
141            case MotionEvent.ACTION_MOVE:
142                if (animView != null) {
143                    final float delta = ev.getX() - mLastX;
144                    animView.setX(animView.getX() + delta);
145                    animView.setAlpha(getAlphaForOffset(animView, thumb.getWidth()));
146                    invalidateGlobalRegion(animView);
147                }
148                mLastX = ev.getX();
149                break;
150
151            case MotionEvent.ACTION_UP:
152                final ObjectAnimator anim;
153                if (animView != null) {
154                    final VelocityTracker velocityTracker = mVelocityTracker;
155                    velocityTracker.computeCurrentVelocity(1000, 10000);
156                    final float velocityX = velocityTracker.getXVelocity();
157                    final float velocityY = velocityTracker.getYVelocity();
158                    final float curX = animView.getX();
159                    final float newX = (velocityX >= 0.0f ? 1 : -1) * animView.getWidth();
160
161                    if (Math.abs(velocityX) > Math.abs(velocityY)
162                            && Math.abs(velocityX) > ESCAPE_VELOCITY
163                            && (velocityX > 0.0f) == (animView.getX() >= 0)) {
164                        final long duration =
165                            (long) (Math.abs(newX-curX) * 1000.0f / Math.abs(velocityX));
166                        anim = ObjectAnimator.ofFloat(animView, "x", curX, newX);
167                        anim.setInterpolator(new LinearInterpolator());
168                        final int swipeDirection = animView.getX() >= 0.0f ?
169                                RecentsCallback.SWIPE_RIGHT : RecentsCallback.SWIPE_LEFT;
170                        anim.addListener(new AnimatorListener() {
171                            public void onAnimationStart(Animator animation) {
172                            }
173                            public void onAnimationRepeat(Animator animation) {
174                            }
175                            public void onAnimationEnd(Animator animation) {
176                                mLinearLayout.removeView(mCurrentView);
177                                mCallback.handleSwipe(animView, swipeDirection);
178                            }
179                            public void onAnimationCancel(Animator animation) {
180                                mLinearLayout.removeView(mCurrentView);
181                                mCallback.handleSwipe(animView, swipeDirection);
182                            }
183                        });
184                        anim.setDuration(duration);
185                    } else { // Animate back to position
186                        final long duration = Math.abs(velocityX) > 0.0f ?
187                                (long) (Math.abs(newX-curX) * 1000.0f / Math.abs(velocityX))
188                                : SNAP_BACK_DURATION;
189                        anim = ObjectAnimator.ofFloat(animView, "x", animView.getX(), 0.0f);
190                        anim.setInterpolator(new DecelerateInterpolator(4.0f));
191                        anim.setDuration(duration);
192                    }
193
194                    anim.addUpdateListener(new AnimatorUpdateListener() {
195                        public void onAnimationUpdate(ValueAnimator animation) {
196                            animView.setAlpha(getAlphaForOffset(animView, thumb.getWidth()));
197                            invalidateGlobalRegion(animView);
198                        }
199                    });
200                    anim.start();
201                }
202
203                mVelocityTracker.recycle();
204                mVelocityTracker = null;
205                break;
206        }
207        return true;
208    }
209
210    void invalidateGlobalRegion(View view) {
211        RectF childBounds
212                = new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
213        childBounds.offset(view.getX(), view.getY());
214        if (DEBUG_INVALIDATE) Log.v(TAG, "-------------");
215        while (view.getParent() != null && view.getParent() instanceof View) {
216            view = (View) view.getParent();
217            view.getMatrix().mapRect(childBounds);
218            view.invalidate((int) Math.floor(childBounds.left),
219                    (int) Math.floor(childBounds.top),
220                    (int) Math.ceil(childBounds.right),
221                    (int) Math.ceil(childBounds.bottom));
222            if (DEBUG_INVALIDATE) {
223                Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
224                        + "," + (int) Math.floor(childBounds.top)
225                        + "," + (int) Math.ceil(childBounds.right)
226                        + "," + (int) Math.ceil(childBounds.bottom));
227            }
228        }
229    }
230
231    @Override
232    protected void onFinishInflate() {
233        super.onFinishInflate();
234        LayoutInflater inflater = (LayoutInflater)
235                mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
236
237        setScrollbarFadingEnabled(true);
238
239        mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout);
240
241        final int leftPadding = mContext.getResources()
242            .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin);
243        setOverScrollEffectPadding(leftPadding, 0);
244    }
245
246    private void setOverScrollEffectPadding(int leftPadding, int i) {
247        // TODO Add to (Vertical)ScrollView
248    }
249
250    @Override
251    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
252        super.onSizeChanged(w, h, oldw, oldh);
253        // Keep track of the last visible item in the list so we can restore it
254        // to the bottom when the orientation changes.
255        mLastScrollPosition = scrollPositionOfMostRecent();
256
257        // This has to happen post-layout, so run it "in the future"
258        post(new Runnable() {
259            public void run() {
260                scrollTo(0, mLastScrollPosition);
261            }
262        });
263    }
264
265    @Override
266    protected void onVisibilityChanged(View changedView, int visibility) {
267        super.onVisibilityChanged(changedView, visibility);
268        // scroll to bottom after reloading
269        if (visibility == View.VISIBLE && changedView == this) {
270            post(new Runnable() {
271                public void run() {
272                    update();
273                }
274            });
275        }
276    }
277
278    public void setAdapter(ActvityDescriptionAdapter adapter) {
279        mAdapter = adapter;
280        mAdapter.registerDataSetObserver(new DataSetObserver() {
281            public void onChanged() {
282                update();
283            }
284
285            public void onInvalidated() {
286                update();
287            }
288        });
289    }
290
291    @Override
292    public void setLayoutTransition(LayoutTransition transition) {
293        // The layout transition applies to our embedded LinearLayout
294        mLinearLayout.setLayoutTransition(transition);
295    }
296
297    public void onClick(View view) {
298        mCallback.handleOnClick(view);
299    }
300
301    public void setCallback(RecentsCallback callback) {
302        mCallback = callback;
303    }
304
305    public boolean onTouch(View v, MotionEvent event) {
306        mCurrentView = v;
307        return false;
308    }
309}
310