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