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.HorizontalScrollView; 34 import android.widget.LinearLayout; 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 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 public View findViewForTask(int persistentTaskId) { 80 for (int i = 0; i < mLinearLayout.getChildCount(); i++) { 81 View v = mLinearLayout.getChildAt(i); 82 RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) v.getTag(); 83 if (holder.taskDescription.persistentTaskId == persistentTaskId) { 84 return v; 85 } 86 } 87 return null; 88 } 89 90 private void update() { 91 for (int i = 0; i < mLinearLayout.getChildCount(); i++) { 92 View v = mLinearLayout.getChildAt(i); 93 addToRecycledViews(v); 94 mAdapter.recycleView(v); 95 } 96 LayoutTransition transitioner = getLayoutTransition(); 97 setLayoutTransition(null); 98 99 mLinearLayout.removeAllViews(); 100 Iterator<View> recycledViews = mRecycledViews.iterator(); 101 for (int i = 0; i < mAdapter.getCount(); i++) { 102 View old = null; 103 if (recycledViews.hasNext()) { 104 old = recycledViews.next(); 105 recycledViews.remove(); 106 old.setVisibility(VISIBLE); 107 } 108 109 final View view = mAdapter.getView(i, old, mLinearLayout); 110 111 if (mPerformanceHelper != null) { 112 mPerformanceHelper.addViewCallback(view); 113 } 114 115 OnTouchListener noOpListener = new OnTouchListener() { 116 @Override 117 public boolean onTouch(View v, MotionEvent event) { 118 return true; 119 } 120 }; 121 122 view.setOnClickListener(new OnClickListener() { 123 public void onClick(View v) { 124 mCallback.dismiss(); 125 } 126 }); 127 // We don't want a click sound when we dimiss recents 128 view.setSoundEffectsEnabled(false); 129 130 OnClickListener launchAppListener = new OnClickListener() { 131 public void onClick(View v) { 132 mCallback.handleOnClick(view); 133 } 134 }; 135 136 RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) view.getTag(); 137 final View thumbnailView = holder.thumbnailView; 138 OnLongClickListener longClickListener = new OnLongClickListener() { 139 public boolean onLongClick(View v) { 140 final View anchorView = view.findViewById(R.id.app_description); 141 mCallback.handleLongPress(view, anchorView, thumbnailView); 142 return true; 143 } 144 }; 145 thumbnailView.setClickable(true); 146 thumbnailView.setOnClickListener(launchAppListener); 147 thumbnailView.setOnLongClickListener(longClickListener); 148 149 // We don't want to dismiss recents if a user clicks on the app title 150 // (we also don't want to launch the app either, though, because the 151 // app title is a small target and doesn't have great click feedback) 152 final View appTitle = view.findViewById(R.id.app_label); 153 appTitle.setContentDescription(" "); 154 appTitle.setOnTouchListener(noOpListener); 155 mLinearLayout.addView(view); 156 } 157 setLayoutTransition(transitioner); 158 159 // Scroll to end after initial layout. 160 161 final OnGlobalLayoutListener updateScroll = new OnGlobalLayoutListener() { 162 public void onGlobalLayout() { 163 mLastScrollPosition = scrollPositionOfMostRecent(); 164 scrollTo(mLastScrollPosition, 0); 165 final ViewTreeObserver observer = getViewTreeObserver(); 166 if (observer.isAlive()) { 167 observer.removeOnGlobalLayoutListener(this); 168 } 169 } 170 }; 171 getViewTreeObserver().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.setTranslationY(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 (x >= item.getLeft() && x < item.getRight() 225 && y >= item.getTop() && y < item.getBottom()) { 226 return item; 227 } 228 } 229 return null; 230 } 231 232 public View getChildContentView(View v) { 233 return v.findViewById(R.id.recent_item); 234 } 235 236 @Override 237 public void draw(Canvas canvas) { 238 super.draw(canvas); 239 240 if (mPerformanceHelper != null) { 241 int paddingLeft = mPaddingLeft; 242 final boolean offsetRequired = isPaddingOffsetRequired(); 243 if (offsetRequired) { 244 paddingLeft += getLeftPaddingOffset(); 245 } 246 247 int left = mScrollX + paddingLeft; 248 int right = left + mRight - mLeft - mPaddingRight - paddingLeft; 249 int top = mScrollY + getFadeTop(offsetRequired); 250 int bottom = top + getFadeHeight(offsetRequired); 251 252 if (offsetRequired) { 253 right += getRightPaddingOffset(); 254 bottom += getBottomPaddingOffset(); 255 } 256 mPerformanceHelper.drawCallback(canvas, 257 left, right, top, bottom, mScrollX, mScrollY, 258 0, 0, 259 getLeftFadingEdgeStrength(), getRightFadingEdgeStrength()); 260 } 261 } 262 263 @Override 264 public int getVerticalFadingEdgeLength() { 265 if (mPerformanceHelper != null) { 266 return mPerformanceHelper.getVerticalFadingEdgeLengthCallback(); 267 } else { 268 return super.getVerticalFadingEdgeLength(); 269 } 270 } 271 272 @Override 273 public int getHorizontalFadingEdgeLength() { 274 if (mPerformanceHelper != null) { 275 return mPerformanceHelper.getHorizontalFadingEdgeLengthCallback(); 276 } else { 277 return super.getHorizontalFadingEdgeLength(); 278 } 279 } 280 281 @Override 282 protected void onFinishInflate() { 283 super.onFinishInflate(); 284 setScrollbarFadingEnabled(true); 285 mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout); 286 final int leftPadding = mContext.getResources() 287 .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin); 288 setOverScrollEffectPadding(leftPadding, 0); 289 } 290 291 @Override 292 public void onAttachedToWindow() { 293 if (mPerformanceHelper != null) { 294 mPerformanceHelper.onAttachedToWindowCallback( 295 mCallback, mLinearLayout, isHardwareAccelerated()); 296 } 297 } 298 299 @Override 300 protected void onConfigurationChanged(Configuration newConfig) { 301 super.onConfigurationChanged(newConfig); 302 float densityScale = getResources().getDisplayMetrics().density; 303 mSwipeHelper.setDensityScale(densityScale); 304 float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); 305 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 306 } 307 308 private void setOverScrollEffectPadding(int leftPadding, int i) { 309 // TODO Add to (Vertical)ScrollView 310 } 311 312 @Override 313 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 314 super.onSizeChanged(w, h, oldw, oldh); 315 316 // Skip this work if a transition is running; it sets the scroll values independently 317 // and should not have those animated values clobbered by this logic 318 LayoutTransition transition = mLinearLayout.getLayoutTransition(); 319 if (transition != null && transition.isRunning()) { 320 return; 321 } 322 // Keep track of the last visible item in the list so we can restore it 323 // to the bottom when the orientation changes. 324 mLastScrollPosition = scrollPositionOfMostRecent(); 325 326 // This has to happen post-layout, so run it "in the future" 327 post(new Runnable() { 328 public void run() { 329 // Make sure we're still not clobbering the transition-set values, since this 330 // runnable launches asynchronously 331 LayoutTransition transition = mLinearLayout.getLayoutTransition(); 332 if (transition == null || !transition.isRunning()) { 333 scrollTo(mLastScrollPosition, 0); 334 } 335 } 336 }); 337 } 338 339 @Override 340 protected void onVisibilityChanged(View changedView, int visibility) { 341 super.onVisibilityChanged(changedView, visibility); 342 // scroll to bottom after reloading 343 if (visibility == View.VISIBLE && changedView == this) { 344 post(new Runnable() { 345 public void run() { 346 update(); 347 } 348 }); 349 } 350 } 351 352 public void setAdapter(TaskDescriptionAdapter adapter) { 353 mAdapter = adapter; 354 mAdapter.registerDataSetObserver(new DataSetObserver() { 355 public void onChanged() { 356 update(); 357 } 358 359 public void onInvalidated() { 360 update(); 361 } 362 }); 363 DisplayMetrics dm = getResources().getDisplayMetrics(); 364 int childWidthMeasureSpec = 365 MeasureSpec.makeMeasureSpec(dm.widthPixels, MeasureSpec.AT_MOST); 366 int childheightMeasureSpec = 367 MeasureSpec.makeMeasureSpec(dm.heightPixels, MeasureSpec.AT_MOST); 368 View child = mAdapter.createView(mLinearLayout); 369 child.measure(childWidthMeasureSpec, childheightMeasureSpec); 370 mNumItemsInOneScreenful = 371 (int) FloatMath.ceil(dm.widthPixels / (float) child.getMeasuredWidth()); 372 addToRecycledViews(child); 373 374 for (int i = 0; i < mNumItemsInOneScreenful - 1; i++) { 375 addToRecycledViews(mAdapter.createView(mLinearLayout)); 376 } 377 } 378 379 public int numItemsInOneScreenful() { 380 return mNumItemsInOneScreenful; 381 } 382 383 @Override 384 public void setLayoutTransition(LayoutTransition transition) { 385 // The layout transition applies to our embedded LinearLayout 386 mLinearLayout.setLayoutTransition(transition); 387 } 388 389 public void setCallback(RecentsCallback callback) { 390 mCallback = callback; 391 } 392 } 393