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