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