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