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 FadedEdgeDrawHelper mFadedEdgeDrawHelper; 53 private HashSet<View> mRecycledViews; 54 private int mNumItemsInOneScreenful; 55 private Runnable mOnScrollListener; 56 57 public RecentsHorizontalScrollView(Context context, AttributeSet attrs) { 58 super(context, attrs, 0); 59 mSwipeHelper = new SwipeHelper(SwipeHelper.Y, this, context); 60 mFadedEdgeDrawHelper = FadedEdgeDrawHelper.create(context, attrs, this, false); 61 mRecycledViews = new HashSet<View>(); 62 } 63 64 public void setMinSwipeAlpha(float minAlpha) { 65 mSwipeHelper.setMinSwipeProgress(minAlpha); 66 } 67 68 private int scrollPositionOfMostRecent() { 69 return mLinearLayout.getWidth() - getWidth(); 70 } 71 72 private void addToRecycledViews(View v) { 73 if (mRecycledViews.size() < mNumItemsInOneScreenful) { 74 mRecycledViews.add(v); 75 } 76 } 77 78 public View findViewForTask(int persistentTaskId) { 79 for (int i = 0; i < mLinearLayout.getChildCount(); i++) { 80 View v = mLinearLayout.getChildAt(i); 81 RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) v.getTag(); 82 if (holder.taskDescription.persistentTaskId == persistentTaskId) { 83 return v; 84 } 85 } 86 return null; 87 } 88 89 private void update() { 90 for (int i = 0; i < mLinearLayout.getChildCount(); i++) { 91 View v = mLinearLayout.getChildAt(i); 92 addToRecycledViews(v); 93 mAdapter.recycleView(v); 94 } 95 LayoutTransition transitioner = getLayoutTransition(); 96 setLayoutTransition(null); 97 98 mLinearLayout.removeAllViews(); 99 Iterator<View> recycledViews = mRecycledViews.iterator(); 100 for (int i = 0; i < mAdapter.getCount(); i++) { 101 View old = null; 102 if (recycledViews.hasNext()) { 103 old = recycledViews.next(); 104 recycledViews.remove(); 105 old.setVisibility(VISIBLE); 106 } 107 108 final View view = mAdapter.getView(i, old, mLinearLayout); 109 110 if (mFadedEdgeDrawHelper != null) { 111 mFadedEdgeDrawHelper.addViewCallback(view); 112 } 113 114 OnTouchListener noOpListener = new OnTouchListener() { 115 @Override 116 public boolean onTouch(View v, MotionEvent event) { 117 return true; 118 } 119 }; 120 121 view.setOnClickListener(new OnClickListener() { 122 public void onClick(View v) { 123 mCallback.dismiss(); 124 } 125 }); 126 // We don't want a click sound when we dimiss recents 127 view.setSoundEffectsEnabled(false); 128 129 OnClickListener launchAppListener = new OnClickListener() { 130 public void onClick(View v) { 131 mCallback.handleOnClick(view); 132 } 133 }; 134 135 RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) view.getTag(); 136 final View thumbnailView = holder.thumbnailView; 137 OnLongClickListener longClickListener = new OnLongClickListener() { 138 public boolean onLongClick(View v) { 139 final View anchorView = view.findViewById(R.id.app_description); 140 mCallback.handleLongPress(view, anchorView, thumbnailView); 141 return true; 142 } 143 }; 144 thumbnailView.setClickable(true); 145 thumbnailView.setOnClickListener(launchAppListener); 146 thumbnailView.setOnLongClickListener(longClickListener); 147 148 // We don't want to dismiss recents if a user clicks on the app title 149 // (we also don't want to launch the app either, though, because the 150 // app title is a small target and doesn't have great click feedback) 151 final View appTitle = view.findViewById(R.id.app_label); 152 appTitle.setContentDescription(" "); 153 appTitle.setOnTouchListener(noOpListener); 154 mLinearLayout.addView(view); 155 } 156 setLayoutTransition(transitioner); 157 158 // Scroll to end after initial layout. 159 160 final OnGlobalLayoutListener updateScroll = new OnGlobalLayoutListener() { 161 public void onGlobalLayout() { 162 mLastScrollPosition = scrollPositionOfMostRecent(); 163 scrollTo(mLastScrollPosition, 0); 164 final ViewTreeObserver observer = getViewTreeObserver(); 165 if (observer.isAlive()) { 166 observer.removeOnGlobalLayoutListener(this); 167 } 168 } 169 }; 170 getViewTreeObserver().addOnGlobalLayoutListener(updateScroll); 171 } 172 173 @Override 174 public void removeViewInLayout(final View view) { 175 dismissChild(view); 176 } 177 178 public boolean onInterceptTouchEvent(MotionEvent ev) { 179 if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()"); 180 return mSwipeHelper.onInterceptTouchEvent(ev) || 181 super.onInterceptTouchEvent(ev); 182 } 183 184 @Override 185 public boolean onTouchEvent(MotionEvent ev) { 186 return mSwipeHelper.onTouchEvent(ev) || 187 super.onTouchEvent(ev); 188 } 189 190 public boolean canChildBeDismissed(View v) { 191 return true; 192 } 193 194 @Override 195 public boolean isAntiFalsingNeeded() { 196 return false; 197 } 198 199 @Override 200 public float getFalsingThresholdFactor() { 201 return 1.0f; 202 } 203 204 public void dismissChild(View v) { 205 mSwipeHelper.dismissChild(v, 0); 206 } 207 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.setTranslationY(0); 217 } 218 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 225 public void onDragCancelled(View v) { 226 } 227 228 @Override 229 public void onChildSnappedBack(View animView) { 230 } 231 232 @Override 233 public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { 234 return false; 235 } 236 237 public View getChildAtPosition(MotionEvent ev) { 238 final float x = ev.getX() + getScrollX(); 239 final float y = ev.getY() + getScrollY(); 240 for (int i = 0; i < mLinearLayout.getChildCount(); i++) { 241 View item = mLinearLayout.getChildAt(i); 242 if (x >= item.getLeft() && x < item.getRight() 243 && y >= item.getTop() && y < item.getBottom()) { 244 return item; 245 } 246 } 247 return null; 248 } 249 250 public View getChildContentView(View v) { 251 return v.findViewById(R.id.recent_item); 252 } 253 254 @Override 255 public void drawFadedEdges(Canvas canvas, int left, int right, int top, int bottom) { 256 if (mFadedEdgeDrawHelper != null) { 257 258 mFadedEdgeDrawHelper.drawCallback(canvas, 259 left, right, top, bottom, getScrollX(), getScrollY(), 260 0, 0, 261 getLeftFadingEdgeStrength(), getRightFadingEdgeStrength(), getPaddingTop()); 262 } 263 } 264 265 @Override 266 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 267 super.onScrollChanged(l, t, oldl, oldt); 268 if (mOnScrollListener != null) { 269 mOnScrollListener.run(); 270 } 271 } 272 273 public void setOnScrollListener(Runnable listener) { 274 mOnScrollListener = listener; 275 } 276 277 @Override 278 public int getVerticalFadingEdgeLength() { 279 if (mFadedEdgeDrawHelper != null) { 280 return mFadedEdgeDrawHelper.getVerticalFadingEdgeLength(); 281 } else { 282 return super.getVerticalFadingEdgeLength(); 283 } 284 } 285 286 @Override 287 public int getHorizontalFadingEdgeLength() { 288 if (mFadedEdgeDrawHelper != null) { 289 return mFadedEdgeDrawHelper.getHorizontalFadingEdgeLength(); 290 } else { 291 return super.getHorizontalFadingEdgeLength(); 292 } 293 } 294 295 @Override 296 protected void onFinishInflate() { 297 super.onFinishInflate(); 298 setScrollbarFadingEnabled(true); 299 mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout); 300 final int leftPadding = getContext().getResources() 301 .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin); 302 setOverScrollEffectPadding(leftPadding, 0); 303 } 304 305 @Override 306 public void onAttachedToWindow() { 307 if (mFadedEdgeDrawHelper != null) { 308 mFadedEdgeDrawHelper.onAttachedToWindowCallback(mLinearLayout, isHardwareAccelerated()); 309 } 310 } 311 312 @Override 313 protected void onConfigurationChanged(Configuration newConfig) { 314 super.onConfigurationChanged(newConfig); 315 float densityScale = getResources().getDisplayMetrics().density; 316 mSwipeHelper.setDensityScale(densityScale); 317 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 318 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 319 } 320 321 private void setOverScrollEffectPadding(int leftPadding, int i) { 322 // TODO Add to (Vertical)ScrollView 323 } 324 325 @Override 326 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 327 super.onSizeChanged(w, h, oldw, oldh); 328 329 // Skip this work if a transition is running; it sets the scroll values independently 330 // and should not have those animated values clobbered by this logic 331 LayoutTransition transition = mLinearLayout.getLayoutTransition(); 332 if (transition != null && transition.isRunning()) { 333 return; 334 } 335 // Keep track of the last visible item in the list so we can restore it 336 // to the bottom when the orientation changes. 337 mLastScrollPosition = scrollPositionOfMostRecent(); 338 339 // This has to happen post-layout, so run it "in the future" 340 post(new Runnable() { 341 public void run() { 342 // Make sure we're still not clobbering the transition-set values, since this 343 // runnable launches asynchronously 344 LayoutTransition transition = mLinearLayout.getLayoutTransition(); 345 if (transition == null || !transition.isRunning()) { 346 scrollTo(mLastScrollPosition, 0); 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