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