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