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 
17 package com.android.systemui.recent;
18 
19 import android.animation.LayoutTransition;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.database.DataSetObserver;
23 import android.graphics.Canvas;
24 import android.util.AttributeSet;
25 import android.util.DisplayMetrics;
26 import android.util.FloatMath;
27 import android.util.Log;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.view.ViewConfiguration;
31 import android.view.ViewTreeObserver;
32 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
33 import android.widget.LinearLayout;
34 import android.widget.ScrollView;
35 
36 import com.android.systemui.R;
37 import com.android.systemui.SwipeHelper;
38 import com.android.systemui.recent.RecentsPanelView.TaskDescriptionAdapter;
39 
40 import java.util.HashSet;
41 import java.util.Iterator;
42 
43 public 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 
RecentsVerticalScrollView(Context context, AttributeSet attrs)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 
setMinSwipeAlpha(float minAlpha)65     public void setMinSwipeAlpha(float minAlpha) {
66         mSwipeHelper.setMinSwipeProgress(minAlpha);
67     }
68 
scrollPositionOfMostRecent()69     private int scrollPositionOfMostRecent() {
70         return mLinearLayout.getHeight() - getHeight() + getPaddingTop();
71     }
72 
addToRecycledViews(View v)73     private void addToRecycledViews(View v) {
74         if (mRecycledViews.size() < mNumItemsInOneScreenful) {
75             mRecycledViews.add(v);
76         }
77     }
78 
findViewForTask(int persistentTaskId)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 
update()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
removeViewInLayout(final View view)182     public void removeViewInLayout(final View view) {
183         dismissChild(view);
184     }
185 
onInterceptTouchEvent(MotionEvent ev)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
onTouchEvent(MotionEvent ev)193     public boolean onTouchEvent(MotionEvent ev) {
194         return mSwipeHelper.onTouchEvent(ev) ||
195             super.onTouchEvent(ev);
196     }
197 
canChildBeDismissed(View v)198     public boolean canChildBeDismissed(View v) {
199         return true;
200     }
201 
202     @Override
isAntiFalsingNeeded()203     public boolean isAntiFalsingNeeded() {
204         return false;
205     }
206 
207     @Override
getFalsingThresholdFactor()208     public float getFalsingThresholdFactor() {
209         return 1.0f;
210     }
211 
dismissChild(View v)212     public void dismissChild(View v) {
213         mSwipeHelper.dismissChild(v, 0);
214     }
215 
onChildDismissed(View v)216     public void onChildDismissed(View v) {
217         addToRecycledViews(v);
218         mLinearLayout.removeView(v);
219         mCallback.handleSwipe(v);
220         // Restore the alpha/translation parameters to what they were before swiping
221         // (for when these items are recycled)
222         View contentView = getChildContentView(v);
223         contentView.setAlpha(1f);
224         contentView.setTranslationX(0);
225     }
226 
onBeginDrag(View v)227     public void onBeginDrag(View v) {
228         // We do this so the underlying ScrollView knows that it won't get
229         // the chance to intercept events anymore
230         requestDisallowInterceptTouchEvent(true);
231     }
232 
onDragCancelled(View v)233     public void onDragCancelled(View v) {
234     }
235 
236     @Override
onChildSnappedBack(View animView)237     public void onChildSnappedBack(View animView) {
238     }
239 
240     @Override
updateSwipeProgress(View animView, boolean dismissable, float swipeProgress)241     public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
242         return false;
243     }
244 
getChildAtPosition(MotionEvent ev)245     public View getChildAtPosition(MotionEvent ev) {
246         final float x = ev.getX() + getScrollX();
247         final float y = ev.getY() + getScrollY();
248         for (int i = 0; i < mLinearLayout.getChildCount(); i++) {
249             View item = mLinearLayout.getChildAt(i);
250             if (item.getVisibility() == View.VISIBLE
251                     && x >= item.getLeft() && x < item.getRight()
252                     && y >= item.getTop() && y < item.getBottom()) {
253                 return item;
254             }
255         }
256         return null;
257     }
258 
getChildContentView(View v)259     public View getChildContentView(View v) {
260         return v.findViewById(R.id.recent_item);
261     }
262 
263     @Override
drawFadedEdges(Canvas canvas, int left, int right, int top, int bottom)264     public void drawFadedEdges(Canvas canvas, int left, int right, int top, int bottom) {
265         if (mFadedEdgeDrawHelper != null) {
266             final boolean offsetRequired = isPaddingOffsetRequired();
267             mFadedEdgeDrawHelper.drawCallback(canvas,
268                     left, right, top + getFadeTop(offsetRequired), bottom, getScrollX(), getScrollY(),
269                     getTopFadingEdgeStrength(), getBottomFadingEdgeStrength(),
270                     0, 0, getPaddingTop());
271         }
272     }
273 
274     @Override
onScrollChanged(int l, int t, int oldl, int oldt)275     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
276        super.onScrollChanged(l, t, oldl, oldt);
277        if (mOnScrollListener != null) {
278            mOnScrollListener.run();
279        }
280     }
281 
setOnScrollListener(Runnable listener)282     public void setOnScrollListener(Runnable listener) {
283         mOnScrollListener = listener;
284     }
285 
286     @Override
getVerticalFadingEdgeLength()287     public int getVerticalFadingEdgeLength() {
288         if (mFadedEdgeDrawHelper != null) {
289             return mFadedEdgeDrawHelper.getVerticalFadingEdgeLength();
290         } else {
291             return super.getVerticalFadingEdgeLength();
292         }
293     }
294 
295     @Override
getHorizontalFadingEdgeLength()296     public int getHorizontalFadingEdgeLength() {
297         if (mFadedEdgeDrawHelper != null) {
298             return mFadedEdgeDrawHelper.getHorizontalFadingEdgeLength();
299         } else {
300             return super.getHorizontalFadingEdgeLength();
301         }
302     }
303 
304     @Override
onFinishInflate()305     protected void onFinishInflate() {
306         super.onFinishInflate();
307         setScrollbarFadingEnabled(true);
308         mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout);
309         final int leftPadding = getContext().getResources()
310             .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin);
311         setOverScrollEffectPadding(leftPadding, 0);
312     }
313 
314     @Override
onAttachedToWindow()315     public void onAttachedToWindow() {
316         if (mFadedEdgeDrawHelper != null) {
317             mFadedEdgeDrawHelper.onAttachedToWindowCallback(mLinearLayout, isHardwareAccelerated());
318         }
319     }
320 
321     @Override
onConfigurationChanged(Configuration newConfig)322     protected void onConfigurationChanged(Configuration newConfig) {
323         super.onConfigurationChanged(newConfig);
324         float densityScale = getResources().getDisplayMetrics().density;
325         mSwipeHelper.setDensityScale(densityScale);
326         float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
327         mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
328     }
329 
setOverScrollEffectPadding(int leftPadding, int i)330     private void setOverScrollEffectPadding(int leftPadding, int i) {
331         // TODO Add to (Vertical)ScrollView
332     }
333 
334     @Override
onSizeChanged(int w, int h, int oldw, int oldh)335     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
336         super.onSizeChanged(w, h, oldw, oldh);
337 
338         // Skip this work if a transition is running; it sets the scroll values independently
339         // and should not have those animated values clobbered by this logic
340         LayoutTransition transition = mLinearLayout.getLayoutTransition();
341         if (transition != null && transition.isRunning()) {
342             return;
343         }
344         // Keep track of the last visible item in the list so we can restore it
345         // to the bottom when the orientation changes.
346         mLastScrollPosition = scrollPositionOfMostRecent();
347 
348         // This has to happen post-layout, so run it "in the future"
349         post(new Runnable() {
350             public void run() {
351                 // Make sure we're still not clobbering the transition-set values, since this
352                 // runnable launches asynchronously
353                 LayoutTransition transition = mLinearLayout.getLayoutTransition();
354                 if (transition == null || !transition.isRunning()) {
355                     scrollTo(0, mLastScrollPosition);
356                 }
357             }
358         });
359     }
360 
setAdapter(TaskDescriptionAdapter adapter)361     public void setAdapter(TaskDescriptionAdapter adapter) {
362         mAdapter = adapter;
363         mAdapter.registerDataSetObserver(new DataSetObserver() {
364             public void onChanged() {
365                 update();
366             }
367 
368             public void onInvalidated() {
369                 update();
370             }
371         });
372 
373         DisplayMetrics dm = getResources().getDisplayMetrics();
374         int childWidthMeasureSpec =
375                 MeasureSpec.makeMeasureSpec(dm.widthPixels, MeasureSpec.AT_MOST);
376         int childheightMeasureSpec =
377                 MeasureSpec.makeMeasureSpec(dm.heightPixels, MeasureSpec.AT_MOST);
378         View child = mAdapter.createView(mLinearLayout);
379         child.measure(childWidthMeasureSpec, childheightMeasureSpec);
380         mNumItemsInOneScreenful =
381                 (int) FloatMath.ceil(dm.heightPixels / (float) child.getMeasuredHeight());
382         addToRecycledViews(child);
383 
384         for (int i = 0; i < mNumItemsInOneScreenful - 1; i++) {
385             addToRecycledViews(mAdapter.createView(mLinearLayout));
386         }
387     }
388 
numItemsInOneScreenful()389     public int numItemsInOneScreenful() {
390         return mNumItemsInOneScreenful;
391     }
392 
393     @Override
setLayoutTransition(LayoutTransition transition)394     public void setLayoutTransition(LayoutTransition transition) {
395         // The layout transition applies to our embedded LinearLayout
396         mLinearLayout.setLayoutTransition(transition);
397     }
398 
setCallback(RecentsCallback callback)399     public void setCallback(RecentsCallback callback) {
400         mCallback = callback;
401     }
402 }
403