RecentsHorizontalScrollView.java revision 99a9655b9333c3bff6e462b12baa56a5fcd4cb20
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 android.animation.LayoutTransition;
20import android.content.Context;
21import android.content.res.Configuration;
22import android.database.DataSetObserver;
23import android.graphics.Canvas;
24import android.util.AttributeSet;
25import android.util.DisplayMetrics;
26import android.util.FloatMath;
27import android.util.Log;
28import android.view.MotionEvent;
29import android.view.View;
30import android.view.View.MeasureSpec;
31import android.view.View.OnClickListener;
32import android.view.View.OnLongClickListener;
33import android.view.View.OnTouchListener;
34import android.view.ViewConfiguration;
35import android.view.ViewTreeObserver;
36import android.view.ViewTreeObserver.OnGlobalLayoutListener;
37import android.widget.HorizontalScrollView;
38import android.widget.LinearLayout;
39
40import com.android.systemui.R;
41import com.android.systemui.SwipeHelper;
42import com.android.systemui.recent.RecentsPanelView.TaskDescriptionAdapter;
43
44import java.util.ArrayList;
45
46public class RecentsHorizontalScrollView extends HorizontalScrollView
47    implements SwipeHelper.Callback {
48    private static final String TAG = RecentsPanelView.TAG;
49    private static final boolean DEBUG = RecentsPanelView.DEBUG;
50    private LinearLayout mLinearLayout;
51    private TaskDescriptionAdapter mAdapter;
52    private RecentsCallback mCallback;
53    protected int mLastScrollPosition;
54    private SwipeHelper mSwipeHelper;
55    private RecentsScrollViewPerformanceHelper mPerformanceHelper;
56    private ArrayList<View> mRecycledViews;
57    private int mNumItemsInOneScreenful;
58
59    public RecentsHorizontalScrollView(Context context, AttributeSet attrs) {
60        super(context, attrs, 0);
61        float densityScale = getResources().getDisplayMetrics().density;
62        float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop();
63        mSwipeHelper = new SwipeHelper(SwipeHelper.Y, this, densityScale, pagingTouchSlop);
64        mPerformanceHelper = RecentsScrollViewPerformanceHelper.create(context, attrs, this, false);
65        mRecycledViews = new ArrayList<View>();
66    }
67
68    private int scrollPositionOfMostRecent() {
69        return mLinearLayout.getWidth() - getWidth();
70    }
71
72    private void update() {
73        for (int i = 0; i < mLinearLayout.getChildCount(); i++) {
74            View v = mLinearLayout.getChildAt(i);
75            mRecycledViews.add(v);
76            mAdapter.recycleView(v);
77        }
78        LayoutTransition transitioner = getLayoutTransition();
79        setLayoutTransition(null);
80
81        mLinearLayout.removeAllViews();
82        for (int i = 0; i < mAdapter.getCount(); i++) {
83            View old = null;
84            if (mRecycledViews.size() != 0) {
85                old = mRecycledViews.remove(mRecycledViews.size() - 1);
86                old.setVisibility(VISIBLE);
87            }
88
89            final View view = mAdapter.getView(i, old, mLinearLayout);
90
91            if (mPerformanceHelper != null) {
92                mPerformanceHelper.addViewCallback(view);
93            }
94
95            OnTouchListener noOpListener = new OnTouchListener() {
96                @Override
97                public boolean onTouch(View v, MotionEvent event) {
98                    return true;
99                }
100            };
101
102            view.setOnClickListener(new OnClickListener() {
103                public void onClick(View v) {
104                    mCallback.dismiss();
105                }
106            });
107            // We don't want a click sound when we dimiss recents
108            view.setSoundEffectsEnabled(false);
109
110            OnClickListener launchAppListener = new OnClickListener() {
111                public void onClick(View v) {
112                    mCallback.handleOnClick(view);
113                }
114            };
115
116            RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) view.getTag();
117            final View thumbnailView = holder.thumbnailView;
118            OnLongClickListener longClickListener = new OnLongClickListener() {
119                public boolean onLongClick(View v) {
120                    final View anchorView = view.findViewById(R.id.app_description);
121                    mCallback.handleLongPress(view, anchorView, thumbnailView);
122                    return true;
123                }
124            };
125            thumbnailView.setClickable(true);
126            thumbnailView.setOnClickListener(launchAppListener);
127            thumbnailView.setOnLongClickListener(longClickListener);
128
129            // We don't want to dismiss recents if a user clicks on the app title
130            // (we also don't want to launch the app either, though, because the
131            // app title is a small target and doesn't have great click feedback)
132            final View appTitle = view.findViewById(R.id.app_label);
133            appTitle.setContentDescription(" ");
134            appTitle.setOnTouchListener(noOpListener);
135            mLinearLayout.addView(view);
136        }
137        setLayoutTransition(transitioner);
138
139        // Scroll to end after layout.
140        final ViewTreeObserver observer = getViewTreeObserver();
141
142        final OnGlobalLayoutListener updateScroll = new OnGlobalLayoutListener() {
143                public void onGlobalLayout() {
144                    mLastScrollPosition = scrollPositionOfMostRecent();
145                    scrollTo(mLastScrollPosition, 0);
146                    if (observer.isAlive()) {
147                        observer.removeOnGlobalLayoutListener(this);
148                    }
149                }
150            };
151        observer.addOnGlobalLayoutListener(updateScroll);
152    }
153
154    @Override
155    public void removeViewInLayout(final View view) {
156        dismissChild(view);
157    }
158
159    public boolean onInterceptTouchEvent(MotionEvent ev) {
160        if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()");
161        return mSwipeHelper.onInterceptTouchEvent(ev) ||
162            super.onInterceptTouchEvent(ev);
163    }
164
165    @Override
166    public boolean onTouchEvent(MotionEvent ev) {
167        return mSwipeHelper.onTouchEvent(ev) ||
168            super.onTouchEvent(ev);
169    }
170
171    public boolean canChildBeDismissed(View v) {
172        return true;
173    }
174
175    public void dismissChild(View v) {
176        mSwipeHelper.dismissChild(v, 0);
177    }
178
179    public void onChildDismissed(View v) {
180        mRecycledViews.add(v);
181        mLinearLayout.removeView(v);
182        mCallback.handleSwipe(v);
183        v.setActivated(false);
184    }
185
186    public void onBeginDrag(View v) {
187        // We do this so the underlying ScrollView knows that it won't get
188        // the chance to intercept events anymore
189        requestDisallowInterceptTouchEvent(true);
190        v.setActivated(true);
191    }
192
193    public void onDragCancelled(View v) {
194        v.setActivated(false);
195    }
196
197    public View getChildAtPosition(MotionEvent ev) {
198        final float x = ev.getX() + getScrollX();
199        final float y = ev.getY() + getScrollY();
200        for (int i = 0; i < mLinearLayout.getChildCount(); i++) {
201            View item = mLinearLayout.getChildAt(i);
202            if (x >= item.getLeft() && x < item.getRight()
203                && y >= item.getTop() && y < item.getBottom()) {
204                return item;
205            }
206        }
207        return null;
208    }
209
210    public View getChildContentView(View v) {
211        return v.findViewById(R.id.recent_item);
212    }
213
214    @Override
215    protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
216        super.onLayout(changed, left, top, right, bottom);
217        if (mPerformanceHelper != null) {
218            mPerformanceHelper.onLayoutCallback();
219        }
220    }
221
222    @Override
223    public void draw(Canvas canvas) {
224        super.draw(canvas);
225
226        if (mPerformanceHelper != null) {
227            int paddingLeft = mPaddingLeft;
228            final boolean offsetRequired = isPaddingOffsetRequired();
229            if (offsetRequired) {
230                paddingLeft += getLeftPaddingOffset();
231            }
232
233            int left = mScrollX + paddingLeft;
234            int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
235            int top = mScrollY + getFadeTop(offsetRequired);
236            int bottom = top + getFadeHeight(offsetRequired);
237
238            if (offsetRequired) {
239                right += getRightPaddingOffset();
240                bottom += getBottomPaddingOffset();
241            }
242            mPerformanceHelper.drawCallback(canvas,
243                    left, right, top, bottom, mScrollX, mScrollY,
244                    0, 0,
245                    getLeftFadingEdgeStrength(), getRightFadingEdgeStrength());
246        }
247    }
248
249    @Override
250    public int getVerticalFadingEdgeLength() {
251        if (mPerformanceHelper != null) {
252            return mPerformanceHelper.getVerticalFadingEdgeLengthCallback();
253        } else {
254            return super.getVerticalFadingEdgeLength();
255        }
256    }
257
258    @Override
259    public int getHorizontalFadingEdgeLength() {
260        if (mPerformanceHelper != null) {
261            return mPerformanceHelper.getHorizontalFadingEdgeLengthCallback();
262        } else {
263            return super.getHorizontalFadingEdgeLength();
264        }
265    }
266
267    @Override
268    protected void onFinishInflate() {
269        super.onFinishInflate();
270        setScrollbarFadingEnabled(true);
271        mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout);
272        final int leftPadding = mContext.getResources()
273            .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin);
274        setOverScrollEffectPadding(leftPadding, 0);
275    }
276
277    @Override
278    public void onAttachedToWindow() {
279        if (mPerformanceHelper != null) {
280            mPerformanceHelper.onAttachedToWindowCallback(
281                    mCallback, mLinearLayout, isHardwareAccelerated());
282        }
283    }
284
285    @Override
286    protected void onConfigurationChanged(Configuration newConfig) {
287        super.onConfigurationChanged(newConfig);
288        float densityScale = getResources().getDisplayMetrics().density;
289        mSwipeHelper.setDensityScale(densityScale);
290        float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop();
291        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
292    }
293
294    private void setOverScrollEffectPadding(int leftPadding, int i) {
295        // TODO Add to (Vertical)ScrollView
296    }
297
298    @Override
299    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
300        super.onSizeChanged(w, h, oldw, oldh);
301
302        // Skip this work if a transition is running; it sets the scroll values independently
303        // and should not have those animated values clobbered by this logic
304        LayoutTransition transition = mLinearLayout.getLayoutTransition();
305        if (transition != null && transition.isRunning()) {
306            return;
307        }
308        // Keep track of the last visible item in the list so we can restore it
309        // to the bottom when the orientation changes.
310        mLastScrollPosition = scrollPositionOfMostRecent();
311
312        // This has to happen post-layout, so run it "in the future"
313        post(new Runnable() {
314            public void run() {
315                // Make sure we're still not clobbering the transition-set values, since this
316                // runnable launches asynchronously
317                LayoutTransition transition = mLinearLayout.getLayoutTransition();
318                if (transition == null || !transition.isRunning()) {
319                    scrollTo(mLastScrollPosition, 0);
320                }
321            }
322        });
323    }
324
325    public void onRecentsVisibilityChanged() {
326        if (mPerformanceHelper != null) {
327            mPerformanceHelper.updateShowBackground();
328        }
329    }
330
331    @Override
332    protected void onVisibilityChanged(View changedView, int visibility) {
333        super.onVisibilityChanged(changedView, visibility);
334        // scroll to bottom after reloading
335        if (visibility == View.VISIBLE && changedView == this) {
336            post(new Runnable() {
337                public void run() {
338                    update();
339                }
340            });
341        }
342    }
343
344    public void setAdapter(TaskDescriptionAdapter adapter) {
345        mAdapter = adapter;
346        mAdapter.registerDataSetObserver(new DataSetObserver() {
347            public void onChanged() {
348                update();
349            }
350
351            public void onInvalidated() {
352                update();
353            }
354        });
355        DisplayMetrics dm = getResources().getDisplayMetrics();
356        int childWidthMeasureSpec =
357                MeasureSpec.makeMeasureSpec(dm.widthPixels, MeasureSpec.AT_MOST);
358        int childheightMeasureSpec =
359                MeasureSpec.makeMeasureSpec(dm.heightPixels, MeasureSpec.AT_MOST);
360        View child = mAdapter.createView(mLinearLayout);
361        child.measure(childWidthMeasureSpec, childheightMeasureSpec);
362        mNumItemsInOneScreenful =
363                (int) FloatMath.ceil(dm.widthPixels / (float) child.getMeasuredWidth());
364        mRecycledViews.add(child);
365
366        for (int i = 0; i < mNumItemsInOneScreenful - 1; i++) {
367            mRecycledViews.add(mAdapter.createView(mLinearLayout));
368        }
369    }
370
371    public int numItemsInOneScreenful() {
372        return mNumItemsInOneScreenful;
373    }
374
375    @Override
376    public void setLayoutTransition(LayoutTransition transition) {
377        if (mPerformanceHelper != null) {
378            mPerformanceHelper.setLayoutTransitionCallback(transition);
379        }
380        // The layout transition applies to our embedded LinearLayout
381        mLinearLayout.setLayoutTransition(transition);
382    }
383
384    public void setCallback(RecentsCallback callback) {
385        mCallback = callback;
386    }
387}
388