1 package com.android.systemui.qs; 2 3 import android.animation.Animator; 4 import android.animation.AnimatorListenerAdapter; 5 import android.animation.AnimatorSet; 6 import android.animation.ObjectAnimator; 7 import android.animation.PropertyValuesHolder; 8 import android.content.Context; 9 import android.content.res.Configuration; 10 import android.content.res.Resources; 11 import android.support.v4.view.PagerAdapter; 12 import android.support.v4.view.ViewPager; 13 import android.util.AttributeSet; 14 import android.util.Log; 15 import android.view.LayoutInflater; 16 import android.view.MotionEvent; 17 import android.view.View; 18 import android.view.ViewGroup; 19 import android.view.animation.Interpolator; 20 import android.view.animation.OvershootInterpolator; 21 import android.widget.Scroller; 22 23 import com.android.systemui.R; 24 import com.android.systemui.qs.QSPanel.QSTileLayout; 25 import com.android.systemui.qs.QSPanel.TileRecord; 26 27 import java.util.ArrayList; 28 import java.util.Set; 29 30 public class PagedTileLayout extends ViewPager implements QSTileLayout { 31 32 private static final boolean DEBUG = false; 33 34 private static final String TAG = "PagedTileLayout"; 35 private static final int REVEAL_SCROLL_DURATION_MILLIS = 750; 36 private static final float BOUNCE_ANIMATION_TENSION = 1.3f; 37 private static final long BOUNCE_ANIMATION_DURATION = 450L; 38 private static final int TILE_ANIMATION_STAGGER_DELAY = 85; 39 private static final Interpolator SCROLL_CUBIC = (t) -> { 40 t -= 1.0f; 41 return t * t * t + 1.0f; 42 }; 43 44 45 private final ArrayList<TileRecord> mTiles = new ArrayList<>(); 46 private final ArrayList<TilePage> mPages = new ArrayList<>(); 47 48 private PageIndicator mPageIndicator; 49 private float mPageIndicatorPosition; 50 51 private int mNumPages; 52 private PageListener mPageListener; 53 54 private boolean mListening; 55 private Scroller mScroller; 56 57 private AnimatorSet mBounceAnimatorSet; 58 private int mAnimatingToPage = -1; 59 private float mLastExpansion; 60 61 public PagedTileLayout(Context context, AttributeSet attrs) { 62 super(context, attrs); 63 mScroller = new Scroller(context, SCROLL_CUBIC); 64 setAdapter(mAdapter); 65 setOnPageChangeListener(mOnPageChangeListener); 66 setCurrentItem(0, false); 67 } 68 69 @Override 70 public void onRtlPropertiesChanged(int layoutDirection) { 71 super.onRtlPropertiesChanged(layoutDirection); 72 setAdapter(mAdapter); 73 setCurrentItem(0, false); 74 } 75 76 @Override 77 public void setCurrentItem(int item, boolean smoothScroll) { 78 if (isLayoutRtl()) { 79 item = mPages.size() - 1 - item; 80 } 81 super.setCurrentItem(item, smoothScroll); 82 } 83 84 @Override 85 public void setListening(boolean listening) { 86 if (mListening == listening) return; 87 mListening = listening; 88 updateListening(); 89 } 90 91 private void updateListening() { 92 for (TilePage tilePage : mPages) { 93 tilePage.setListening(tilePage.getParent() == null ? false : mListening); 94 } 95 } 96 97 @Override 98 public boolean onInterceptTouchEvent(MotionEvent ev) { 99 // Suppress all touch event during reveal animation. 100 if (mAnimatingToPage != -1) { 101 return true; 102 } 103 return super.onInterceptTouchEvent(ev); 104 } 105 106 @Override 107 public boolean onTouchEvent(MotionEvent ev) { 108 // Suppress all touch event during reveal animation. 109 if (mAnimatingToPage != -1) { 110 return true; 111 } 112 return super.onTouchEvent(ev); 113 } 114 115 @Override 116 public void computeScroll() { 117 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { 118 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 119 float pageFraction = (float) getScrollX() / getWidth(); 120 int position = (int) pageFraction; 121 float positionOffset = pageFraction - position; 122 mOnPageChangeListener.onPageScrolled(position, positionOffset, getScrollX()); 123 // Keep on drawing until the animation has finished. 124 postInvalidateOnAnimation(); 125 return; 126 } 127 if (mAnimatingToPage != -1) { 128 setCurrentItem(mAnimatingToPage, true); 129 mBounceAnimatorSet.start(); 130 setOffscreenPageLimit(1); 131 mAnimatingToPage = -1; 132 } 133 super.computeScroll(); 134 } 135 136 @Override 137 public boolean hasOverlappingRendering() { 138 return false; 139 } 140 141 @Override 142 protected void onFinishInflate() { 143 super.onFinishInflate(); 144 mPages.add((TilePage) LayoutInflater.from(getContext()) 145 .inflate(R.layout.qs_paged_page, this, false)); 146 } 147 148 public void setPageIndicator(PageIndicator indicator) { 149 mPageIndicator = indicator; 150 mPageIndicator.setNumPages(mNumPages); 151 mPageIndicator.setLocation(mPageIndicatorPosition); 152 } 153 154 @Override 155 public int getOffsetTop(TileRecord tile) { 156 final ViewGroup parent = (ViewGroup) tile.tileView.getParent(); 157 if (parent == null) return 0; 158 return parent.getTop() + getTop(); 159 } 160 161 @Override 162 public void addTile(TileRecord tile) { 163 mTiles.add(tile); 164 postDistributeTiles(); 165 } 166 167 @Override 168 public void removeTile(TileRecord tile) { 169 if (mTiles.remove(tile)) { 170 postDistributeTiles(); 171 } 172 } 173 174 @Override 175 public void setExpansion(float expansion) { 176 mLastExpansion = expansion; 177 updateSelected(); 178 } 179 180 private void updateSelected() { 181 // Start the marquee when fully expanded and stop when fully collapsed. Leave as is for 182 // other expansion ratios since there is no way way to pause the marquee. 183 if (mLastExpansion > 0f && mLastExpansion < 1f) { 184 return; 185 } 186 boolean selected = mLastExpansion == 1f; 187 188 // Disable accessibility temporarily while we update selected state purely for the 189 // marquee. This will ensure that accessibility doesn't announce the TYPE_VIEW_SELECTED 190 // event on any of the children. 191 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 192 for (int i = 0; i < mPages.size(); i++) { 193 mPages.get(i).setSelected(i == getCurrentItem() ? selected : false); 194 } 195 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); 196 } 197 198 public void setPageListener(PageListener listener) { 199 mPageListener = listener; 200 } 201 202 private void postDistributeTiles() { 203 removeCallbacks(mDistribute); 204 post(mDistribute); 205 } 206 207 private void distributeTiles() { 208 if (DEBUG) Log.d(TAG, "Distributing tiles"); 209 final int NP = mPages.size(); 210 for (int i = 0; i < NP; i++) { 211 mPages.get(i).removeAllViews(); 212 } 213 int index = 0; 214 final int NT = mTiles.size(); 215 for (int i = 0; i < NT; i++) { 216 TileRecord tile = mTiles.get(i); 217 if (mPages.get(index).isFull()) { 218 if (++index == mPages.size()) { 219 if (DEBUG) Log.d(TAG, "Adding page for " 220 + tile.tile.getClass().getSimpleName()); 221 mPages.add((TilePage) LayoutInflater.from(getContext()) 222 .inflate(R.layout.qs_paged_page, this, false)); 223 } 224 } 225 if (DEBUG) Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to " 226 + index); 227 mPages.get(index).addTile(tile); 228 } 229 if (mNumPages != index + 1) { 230 mNumPages = index + 1; 231 while (mPages.size() > mNumPages) { 232 mPages.remove(mPages.size() - 1); 233 } 234 if (DEBUG) Log.d(TAG, "Size: " + mNumPages); 235 mPageIndicator.setNumPages(mNumPages); 236 setAdapter(mAdapter); 237 mAdapter.notifyDataSetChanged(); 238 setCurrentItem(0, false); 239 } 240 } 241 242 @Override 243 public boolean updateResources() { 244 // Update bottom padding, useful for removing extra space once the panel page indicator is 245 // hidden. 246 setPadding(0, 0, 0, 247 getContext().getResources().getDimensionPixelSize( 248 R.dimen.qs_paged_tile_layout_padding_bottom)); 249 250 boolean changed = false; 251 for (int i = 0; i < mPages.size(); i++) { 252 changed |= mPages.get(i).updateResources(); 253 } 254 if (changed) { 255 distributeTiles(); 256 } 257 return changed; 258 } 259 260 @Override 261 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 262 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 263 // The ViewPager likes to eat all of the space, instead force it to wrap to the max height 264 // of the pages. 265 int maxHeight = 0; 266 final int N = getChildCount(); 267 for (int i = 0; i < N; i++) { 268 int height = getChildAt(i).getMeasuredHeight(); 269 if (height > maxHeight) { 270 maxHeight = height; 271 } 272 } 273 setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom()); 274 } 275 276 private final Runnable mDistribute = new Runnable() { 277 @Override 278 public void run() { 279 distributeTiles(); 280 } 281 }; 282 283 public int getColumnCount() { 284 if (mPages.size() == 0) return 0; 285 return mPages.get(0).mColumns; 286 } 287 288 public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) { 289 if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0) { 290 // Do not start the reveal animation unless there are tiles to animate, multiple 291 // TilePages available and the user has not already started dragging. 292 return; 293 } 294 295 final int lastPageNumber = mPages.size() - 1; 296 final TilePage lastPage = mPages.get(lastPageNumber); 297 final ArrayList<Animator> bounceAnims = new ArrayList<>(); 298 for (TileRecord tr : lastPage.mRecords) { 299 if (tileSpecs.contains(tr.tile.getTileSpec())) { 300 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size())); 301 } 302 } 303 304 if (bounceAnims.isEmpty()) { 305 // All tileSpecs are on the first page. Nothing to do. 306 // TODO: potentially show a bounce animation for first page QS tiles 307 return; 308 } 309 310 mBounceAnimatorSet = new AnimatorSet(); 311 mBounceAnimatorSet.playTogether(bounceAnims); 312 mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() { 313 @Override 314 public void onAnimationEnd(Animator animation) { 315 mBounceAnimatorSet = null; 316 postAnimation.run(); 317 } 318 }); 319 mAnimatingToPage = lastPageNumber; 320 setOffscreenPageLimit(mAnimatingToPage); // Ensure the page to reveal has been inflated. 321 mScroller.startScroll(getScrollX(), getScrollY(), getWidth() * mAnimatingToPage, 0, 322 REVEAL_SCROLL_DURATION_MILLIS); 323 postInvalidateOnAnimation(); 324 } 325 326 private static Animator setupBounceAnimator(View view, int ordinal) { 327 view.setAlpha(0f); 328 view.setScaleX(0f); 329 view.setScaleY(0f); 330 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, 331 PropertyValuesHolder.ofFloat(View.ALPHA, 1), 332 PropertyValuesHolder.ofFloat(View.SCALE_X, 1), 333 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1)); 334 animator.setDuration(BOUNCE_ANIMATION_DURATION); 335 animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY); 336 animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION)); 337 return animator; 338 } 339 340 private final ViewPager.OnPageChangeListener mOnPageChangeListener = 341 new ViewPager.SimpleOnPageChangeListener() { 342 @Override 343 public void onPageSelected(int position) { 344 updateSelected(); 345 if (mPageIndicator == null) return; 346 if (mPageListener != null) { 347 mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1 348 : position == 0); 349 } 350 } 351 352 @Override 353 public void onPageScrolled(int position, float positionOffset, 354 int positionOffsetPixels) { 355 if (mPageIndicator == null) return; 356 mPageIndicatorPosition = position + positionOffset; 357 mPageIndicator.setLocation(mPageIndicatorPosition); 358 if (mPageListener != null) { 359 mPageListener.onPageChanged(positionOffsetPixels == 0 && 360 (isLayoutRtl() ? position == mPages.size() - 1 : position == 0)); 361 } 362 } 363 }; 364 365 public static class TilePage extends TileLayout { 366 private int mMaxRows = 3; 367 public TilePage(Context context, AttributeSet attrs) { 368 super(context, attrs); 369 updateResources(); 370 } 371 372 @Override 373 public boolean updateResources() { 374 final int rows = getRows(); 375 boolean changed = rows != mMaxRows; 376 if (changed) { 377 mMaxRows = rows; 378 requestLayout(); 379 } 380 return super.updateResources() || changed; 381 } 382 383 private int getRows() { 384 return Math.max(1, getResources().getInteger(R.integer.quick_settings_num_rows)); 385 } 386 387 public void setMaxRows(int maxRows) { 388 mMaxRows = maxRows; 389 } 390 391 public boolean isFull() { 392 return mRecords.size() >= mColumns * mMaxRows; 393 } 394 } 395 396 private final PagerAdapter mAdapter = new PagerAdapter() { 397 @Override 398 public void destroyItem(ViewGroup container, int position, Object object) { 399 if (DEBUG) Log.d(TAG, "Destantiating " + position); 400 container.removeView((View) object); 401 updateListening(); 402 } 403 404 @Override 405 public Object instantiateItem(ViewGroup container, int position) { 406 if (DEBUG) Log.d(TAG, "Instantiating " + position); 407 if (isLayoutRtl()) { 408 position = mPages.size() - 1 - position; 409 } 410 ViewGroup view = mPages.get(position); 411 container.addView(view); 412 updateListening(); 413 return view; 414 } 415 416 @Override 417 public int getCount() { 418 return mNumPages; 419 } 420 421 @Override 422 public boolean isViewFromObject(View view, Object object) { 423 return view == object; 424 } 425 }; 426 427 public interface PageListener { 428 void onPageChanged(boolean isFirst); 429 } 430 } 431