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