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