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