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