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 RecentsScrollViewPerformanceHelper mPerformanceHelper; 53 private HashSet<View> mRecycledViews; 54 private int mNumItemsInOneScreenful; 55 56 public RecentsVerticalScrollView(Context context, AttributeSet attrs) { 57 super(context, attrs, 0); 58 float densityScale = getResources().getDisplayMetrics().density; 59 float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); 60 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); 61 62 mPerformanceHelper = RecentsScrollViewPerformanceHelper.create(context, attrs, this, true); 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.getHeight() - getHeight(); 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 102 // Once we can clear the data associated with individual item views, 103 // we can get rid of the removeAllViews() and the code below will 104 // recycle them. 105 Iterator<View> recycledViews = mRecycledViews.iterator(); 106 for (int i = 0; i < mAdapter.getCount(); i++) { 107 View old = null; 108 if (recycledViews.hasNext()) { 109 old = recycledViews.next(); 110 recycledViews.remove(); 111 old.setVisibility(VISIBLE); 112 } 113 final View view = mAdapter.getView(i, old, mLinearLayout); 114 115 if (mPerformanceHelper != null) { 116 mPerformanceHelper.addViewCallback(view); 117 } 118 119 OnTouchListener noOpListener = new OnTouchListener() { 120 @Override 121 public boolean onTouch(View v, MotionEvent event) { 122 return true; 123 } 124 }; 125 126 view.setOnClickListener(new OnClickListener() { 127 public void onClick(View v) { 128 mCallback.dismiss(); 129 } 130 }); 131 // We don't want a click sound when we dimiss recents 132 view.setSoundEffectsEnabled(false); 133 134 OnClickListener launchAppListener = new OnClickListener() { 135 public void onClick(View v) { 136 mCallback.handleOnClick(view); 137 } 138 }; 139 140 RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) view.getTag(); 141 final View thumbnailView = holder.thumbnailView; 142 OnLongClickListener longClickListener = new OnLongClickListener() { 143 public boolean onLongClick(View v) { 144 final View anchorView = view.findViewById(R.id.app_description); 145 mCallback.handleLongPress(view, anchorView, thumbnailView); 146 return true; 147 } 148 }; 149 thumbnailView.setClickable(true); 150 thumbnailView.setOnClickListener(launchAppListener); 151 thumbnailView.setOnLongClickListener(longClickListener); 152 153 // We don't want to dismiss recents if a user clicks on the app title 154 // (we also don't want to launch the app either, though, because the 155 // app title is a small target and doesn't have great click feedback) 156 final View appTitle = view.findViewById(R.id.app_label); 157 appTitle.setContentDescription(" "); 158 appTitle.setOnTouchListener(noOpListener); 159 final View calloutLine = view.findViewById(R.id.recents_callout_line); 160 if (calloutLine != null) { 161 calloutLine.setOnTouchListener(noOpListener); 162 } 163 164 mLinearLayout.addView(view); 165 } 166 setLayoutTransition(transitioner); 167 168 // Scroll to end after initial layout. 169 final OnGlobalLayoutListener updateScroll = new OnGlobalLayoutListener() { 170 public void onGlobalLayout() { 171 mLastScrollPosition = scrollPositionOfMostRecent(); 172 scrollTo(0, mLastScrollPosition); 173 final ViewTreeObserver observer = getViewTreeObserver(); 174 if (observer.isAlive()) { 175 observer.removeOnGlobalLayoutListener(this); 176 } 177 } 178 }; 179 getViewTreeObserver().addOnGlobalLayoutListener(updateScroll); 180 } 181 182 @Override 183 public void removeViewInLayout(final View view) { 184 dismissChild(view); 185 } 186 187 public boolean onInterceptTouchEvent(MotionEvent ev) { 188 if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()"); 189 return mSwipeHelper.onInterceptTouchEvent(ev) || 190 super.onInterceptTouchEvent(ev); 191 } 192 193 @Override 194 public boolean onTouchEvent(MotionEvent ev) { 195 return mSwipeHelper.onTouchEvent(ev) || 196 super.onTouchEvent(ev); 197 } 198 199 public boolean canChildBeDismissed(View v) { 200 return true; 201 } 202 203 public void dismissChild(View v) { 204 mSwipeHelper.dismissChild(v, 0); 205 } 206 207 public void onChildDismissed(View v) { 208 addToRecycledViews(v); 209 mLinearLayout.removeView(v); 210 mCallback.handleSwipe(v); 211 // Restore the alpha/translation parameters to what they were before swiping 212 // (for when these items are recycled) 213 View contentView = getChildContentView(v); 214 contentView.setAlpha(1f); 215 contentView.setTranslationX(0); 216 } 217 218 public void onBeginDrag(View v) { 219 // We do this so the underlying ScrollView knows that it won't get 220 // the chance to intercept events anymore 221 requestDisallowInterceptTouchEvent(true); 222 } 223 224 public void onDragCancelled(View v) { 225 } 226 227 public View getChildAtPosition(MotionEvent ev) { 228 final float x = ev.getX() + getScrollX(); 229 final float y = ev.getY() + getScrollY(); 230 for (int i = 0; i < mLinearLayout.getChildCount(); i++) { 231 View item = mLinearLayout.getChildAt(i); 232 if (item.getVisibility() == View.VISIBLE 233 && x >= item.getLeft() && x < item.getRight() 234 && y >= item.getTop() && y < item.getBottom()) { 235 return item; 236 } 237 } 238 return null; 239 } 240 241 public View getChildContentView(View v) { 242 return v.findViewById(R.id.recent_item); 243 } 244 245 @Override 246 public void draw(Canvas canvas) { 247 super.draw(canvas); 248 249 if (mPerformanceHelper != null) { 250 int paddingLeft = mPaddingLeft; 251 final boolean offsetRequired = isPaddingOffsetRequired(); 252 if (offsetRequired) { 253 paddingLeft += getLeftPaddingOffset(); 254 } 255 256 int left = mScrollX + paddingLeft; 257 int right = left + mRight - mLeft - mPaddingRight - paddingLeft; 258 int top = mScrollY + getFadeTop(offsetRequired); 259 int bottom = top + getFadeHeight(offsetRequired); 260 261 if (offsetRequired) { 262 right += getRightPaddingOffset(); 263 bottom += getBottomPaddingOffset(); 264 } 265 mPerformanceHelper.drawCallback(canvas, 266 left, right, top, bottom, mScrollX, mScrollY, 267 getTopFadingEdgeStrength(), getBottomFadingEdgeStrength(), 268 0, 0); 269 } 270 } 271 272 @Override 273 public int getVerticalFadingEdgeLength() { 274 if (mPerformanceHelper != null) { 275 return mPerformanceHelper.getVerticalFadingEdgeLengthCallback(); 276 } else { 277 return super.getVerticalFadingEdgeLength(); 278 } 279 } 280 281 @Override 282 public int getHorizontalFadingEdgeLength() { 283 if (mPerformanceHelper != null) { 284 return mPerformanceHelper.getHorizontalFadingEdgeLengthCallback(); 285 } else { 286 return super.getHorizontalFadingEdgeLength(); 287 } 288 } 289 290 @Override 291 protected void onFinishInflate() { 292 super.onFinishInflate(); 293 setScrollbarFadingEnabled(true); 294 mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout); 295 final int leftPadding = mContext.getResources() 296 .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin); 297 setOverScrollEffectPadding(leftPadding, 0); 298 } 299 300 @Override 301 public void onAttachedToWindow() { 302 if (mPerformanceHelper != null) { 303 mPerformanceHelper.onAttachedToWindowCallback( 304 mCallback, mLinearLayout, isHardwareAccelerated()); 305 } 306 } 307 308 @Override 309 protected void onConfigurationChanged(Configuration newConfig) { 310 super.onConfigurationChanged(newConfig); 311 float densityScale = getResources().getDisplayMetrics().density; 312 mSwipeHelper.setDensityScale(densityScale); 313 float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); 314 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 315 } 316 317 private void setOverScrollEffectPadding(int leftPadding, int i) { 318 // TODO Add to (Vertical)ScrollView 319 } 320 321 @Override 322 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 323 super.onSizeChanged(w, h, oldw, oldh); 324 325 // Skip this work if a transition is running; it sets the scroll values independently 326 // and should not have those animated values clobbered by this logic 327 LayoutTransition transition = mLinearLayout.getLayoutTransition(); 328 if (transition != null && transition.isRunning()) { 329 return; 330 } 331 // Keep track of the last visible item in the list so we can restore it 332 // to the bottom when the orientation changes. 333 mLastScrollPosition = scrollPositionOfMostRecent(); 334 335 // This has to happen post-layout, so run it "in the future" 336 post(new Runnable() { 337 public void run() { 338 // Make sure we're still not clobbering the transition-set values, since this 339 // runnable launches asynchronously 340 LayoutTransition transition = mLinearLayout.getLayoutTransition(); 341 if (transition == null || !transition.isRunning()) { 342 scrollTo(0, mLastScrollPosition); 343 } 344 } 345 }); 346 } 347 348 @Override 349 protected void onVisibilityChanged(View changedView, int visibility) { 350 super.onVisibilityChanged(changedView, visibility); 351 // scroll to bottom after reloading 352 if (visibility == View.VISIBLE && changedView == this) { 353 post(new Runnable() { 354 public void run() { 355 update(); 356 } 357 }); 358 } 359 } 360 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 389 public int numItemsInOneScreenful() { 390 return mNumItemsInOneScreenful; 391 } 392 393 @Override 394 public void setLayoutTransition(LayoutTransition transition) { 395 // The layout transition applies to our embedded LinearLayout 396 mLinearLayout.setLayoutTransition(transition); 397 } 398 399 public void setCallback(RecentsCallback callback) { 400 mCallback = callback; 401 } 402 } 403