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