RecentsHorizontalScrollView.java revision 17dfec7111fcc53a4f6ae6e92b4a7f85a278fe71
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 com.android.systemui.recent.RecentsPanelView.ActivityDescriptionAdapter; 20 21import android.animation.Animator; 22import android.animation.Animator.AnimatorListener; 23import android.animation.LayoutTransition; 24import android.animation.ObjectAnimator; 25import android.animation.ValueAnimator; 26import android.animation.ValueAnimator.AnimatorUpdateListener; 27import android.content.Context; 28import android.content.res.Configuration; 29import android.database.DataSetObserver; 30import android.graphics.RectF; 31import android.util.AttributeSet; 32import android.util.Log; 33import android.view.LayoutInflater; 34import android.view.MotionEvent; 35import android.view.VelocityTracker; 36import android.view.View; 37import android.view.ViewConfiguration; 38import android.view.animation.DecelerateInterpolator; 39import android.view.animation.LinearInterpolator; 40import android.widget.HorizontalScrollView; 41import android.widget.LinearLayout; 42 43import com.android.systemui.R; 44 45public class RecentsHorizontalScrollView extends HorizontalScrollView 46 implements View.OnClickListener, View.OnTouchListener { 47 private static final boolean DEBUG_INVALIDATE = false; 48 private static final String TAG = RecentsPanelView.TAG; 49 private static final boolean DEBUG = RecentsPanelView.DEBUG; 50 private LinearLayout mLinearLayout; 51 private ActivityDescriptionAdapter mAdapter; 52 private RecentsCallback mCallback; 53 protected int mLastScrollPosition; 54 private View mCurrentView; 55 private float mLastY; 56 private boolean mDragging; 57 private VelocityTracker mVelocityTracker; 58 private float mDensityScale; 59 private float mPagingTouchSlop; 60 61 public RecentsHorizontalScrollView(Context context) { 62 this(context, null); 63 } 64 65 public RecentsHorizontalScrollView(Context context, AttributeSet attrs) { 66 super(context, attrs, 0); 67 mDensityScale = getResources().getDisplayMetrics().density; 68 mPagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); 69 } 70 71 private int scrollPositionOfMostRecent() { 72 return mLinearLayout.getWidth() - getWidth(); 73 } 74 75 public void update() { 76 mLinearLayout.removeAllViews(); 77 for (int i = 0; i < mAdapter.getCount(); i++) { 78 View view = mAdapter.getView(i, null, mLinearLayout); 79 view.setClickable(true); 80 view.setOnClickListener(this); 81 view.setOnTouchListener(this); 82 mLinearLayout.addView(view); 83 } 84 // Scroll to end after layout. 85 post(new Runnable() { 86 public void run() { 87 mLastScrollPosition = scrollPositionOfMostRecent(); 88 scrollTo(mLastScrollPosition, 0); 89 } 90 }); 91 } 92 93 @Override 94 public boolean onInterceptTouchEvent(MotionEvent ev) { 95 if (mVelocityTracker == null) { 96 mVelocityTracker = VelocityTracker.obtain(); 97 } 98 mVelocityTracker.addMovement(ev); 99 switch (ev.getAction()) { 100 case MotionEvent.ACTION_DOWN: 101 mDragging = false; 102 mLastY = ev.getY(); 103 break; 104 105 case MotionEvent.ACTION_MOVE: 106 float delta = ev.getY() - mLastY; 107 if (DEBUG) Log.v(TAG, "ACTION_MOVE : " + delta); 108 if (Math.abs(delta) > mPagingTouchSlop) { 109 mDragging = true; 110 } 111 break; 112 113 case MotionEvent.ACTION_UP: 114 mDragging = false; 115 break; 116 } 117 return mDragging ? true : super.onInterceptTouchEvent(ev); 118 } 119 120 private float getAlphaForOffset(View view, float thumbHeight) { 121 final float fadeHeight = Constants.ALPHA_FADE_END * thumbHeight; 122 float result = 1.0f; 123 if (view.getY() >= thumbHeight * Constants.ALPHA_FADE_START) { 124 result = 1.0f - (view.getY() - thumbHeight * Constants.ALPHA_FADE_START) / fadeHeight; 125 } else if (view.getY() < thumbHeight * (1.0f - Constants.ALPHA_FADE_START)) { 126 result = 1.0f + (thumbHeight * Constants.ALPHA_FADE_START + view.getY()) / fadeHeight; 127 } 128 if (DEBUG) Log.v(TAG, "FADE AMOUNT: " + result); 129 return result; 130 } 131 132 @Override 133 public boolean onTouchEvent(MotionEvent ev) { 134 if (!mDragging) { 135 return super.onTouchEvent(ev); 136 } 137 138 mVelocityTracker.addMovement(ev); 139 140 final View animView = mCurrentView; 141 // TODO: Cache thumbnail 142 final View thumb = animView.findViewById(R.id.app_thumbnail); 143 switch (ev.getAction()) { 144 case MotionEvent.ACTION_MOVE: 145 if (animView != null) { 146 final float delta = ev.getY() - mLastY; 147 animView.setY(animView.getY() + delta); 148 animView.setAlpha(getAlphaForOffset(animView, thumb.getHeight())); 149 invalidateGlobalRegion(animView); 150 } 151 mLastY = ev.getY(); 152 break; 153 154 case MotionEvent.ACTION_UP: 155 final ObjectAnimator anim; 156 if (animView != null) { 157 final VelocityTracker velocityTracker = mVelocityTracker; 158 velocityTracker.computeCurrentVelocity(1000, 10000); 159 final float velocityX = velocityTracker.getXVelocity(); 160 final float velocityY = velocityTracker.getYVelocity(); 161 final float curY = animView.getY(); 162 final float newY = (velocityY >= 0.0f ? 1 : -1) * animView.getHeight(); 163 final float maxVelocity = Constants.ESCAPE_VELOCITY * mDensityScale; 164 if (Math.abs(velocityY) > Math.abs(velocityX) 165 && Math.abs(velocityY) > maxVelocity 166 && (velocityY >= 0.0f) == (animView.getY() >= 0)) { 167 long duration = 168 (long) (Math.abs(newY - curY) * 1000.0f / Math.abs(velocityY)); 169 duration = Math.min(duration, Constants.MAX_ESCAPE_ANIMATION_DURATION); 170 anim = ObjectAnimator.ofFloat(animView, "y", curY, newY); 171 anim.setInterpolator(new LinearInterpolator()); 172 final int swipeDirection = animView.getY() >= 0.0f ? 173 RecentsCallback.SWIPE_RIGHT : RecentsCallback.SWIPE_LEFT; 174 anim.addListener(new AnimatorListener() { 175 public void onAnimationStart(Animator animation) { 176 } 177 public void onAnimationRepeat(Animator animation) { 178 } 179 public void onAnimationEnd(Animator animation) { 180 mLinearLayout.removeView(mCurrentView); 181 mCallback.handleSwipe(animView, swipeDirection); 182 } 183 public void onAnimationCancel(Animator animation) { 184 mLinearLayout.removeView(mCurrentView); 185 mCallback.handleSwipe(animView, swipeDirection); 186 } 187 }); 188 anim.setDuration(duration); 189 } else { // Animate back to position 190 long duration = Math.abs(velocityY) > 0.0f ? 191 (long) (Math.abs(newY - curY) * 1000.0f / Math.abs(velocityY)) 192 : Constants.SNAP_BACK_DURATION; 193 duration = Math.min(duration, Constants.SNAP_BACK_DURATION); 194 anim = ObjectAnimator.ofFloat(animView, "y", animView.getY(), 0.0f); 195 anim.setInterpolator(new DecelerateInterpolator(2.0f)); 196 anim.setDuration(duration); 197 } 198 199 anim.addUpdateListener(new AnimatorUpdateListener() { 200 public void onAnimationUpdate(ValueAnimator animation) { 201 animView.setAlpha(getAlphaForOffset(animView, thumb.getHeight())); 202 invalidateGlobalRegion(animView); 203 } 204 }); 205 anim.start(); 206 } 207 208 mVelocityTracker.recycle(); 209 mVelocityTracker = null; 210 break; 211 } 212 return true; 213 } 214 215 void invalidateGlobalRegion(View view) { 216 RectF childBounds 217 = new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); 218 childBounds.offset(view.getX(), view.getY()); 219 if (DEBUG_INVALIDATE) Log.v(TAG, "-------------"); 220 while (view.getParent() != null && view.getParent() instanceof View) { 221 view = (View) view.getParent(); 222 view.getMatrix().mapRect(childBounds); 223 view.invalidate((int) Math.floor(childBounds.left), 224 (int) Math.floor(childBounds.top), 225 (int) Math.ceil(childBounds.right), 226 (int) Math.ceil(childBounds.bottom)); 227 if (DEBUG_INVALIDATE) { 228 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left) 229 + "," + (int) Math.floor(childBounds.top) 230 + "," + (int) Math.ceil(childBounds.right) 231 + "," + (int) Math.ceil(childBounds.bottom)); 232 } 233 } 234 } 235 236 @Override 237 protected void onFinishInflate() { 238 super.onFinishInflate(); 239 LayoutInflater inflater = (LayoutInflater) 240 mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 241 242 setScrollbarFadingEnabled(true); 243 244 mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout); 245 246 final int leftPadding = mContext.getResources() 247 .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin); 248 setOverScrollEffectPadding(leftPadding, 0); 249 } 250 251 @Override 252 protected void onConfigurationChanged(Configuration newConfig) { 253 super.onConfigurationChanged(newConfig); 254 mDensityScale = getResources().getDisplayMetrics().density; 255 mPagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); 256 } 257 258 private void setOverScrollEffectPadding(int leftPadding, int i) { 259 // TODO Add to (Vertical)ScrollView 260 } 261 262 @Override 263 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 264 super.onSizeChanged(w, h, oldw, oldh); 265 // Keep track of the last visible item in the list so we can restore it 266 // to the bottom when the orientation changes. 267 mLastScrollPosition = scrollPositionOfMostRecent(); 268 269 // This has to happen post-layout, so run it "in the future" 270 post(new Runnable() { 271 public void run() { 272 scrollTo(mLastScrollPosition, 0); 273 } 274 }); 275 } 276 277 @Override 278 protected void onVisibilityChanged(View changedView, int visibility) { 279 super.onVisibilityChanged(changedView, visibility); 280 // scroll to bottom after reloading 281 if (visibility == View.VISIBLE && changedView == this) { 282 post(new Runnable() { 283 public void run() { 284 update(); 285 } 286 }); 287 } 288 } 289 290 public void setAdapter(ActivityDescriptionAdapter adapter) { 291 mAdapter = adapter; 292 mAdapter.registerDataSetObserver(new DataSetObserver() { 293 public void onChanged() { 294 update(); 295 } 296 297 public void onInvalidated() { 298 update(); 299 } 300 }); 301 } 302 303 @Override 304 public void setLayoutTransition(LayoutTransition transition) { 305 // The layout transition applies to our embedded LinearLayout 306 mLinearLayout.setLayoutTransition(transition); 307 } 308 309 public void onClick(View view) { 310 mCallback.handleOnClick(view); 311 } 312 313 public void setCallback(RecentsCallback callback) { 314 mCallback = callback; 315 } 316 317 public boolean onTouch(View v, MotionEvent event) { 318 mCurrentView = v; 319 return false; 320 } 321} 322