blob: ab822696d38264b10d3fc20e23866c237a5da1fb [file] [log] [blame]
Jason Monkcaf37622015-08-18 12:33:50 -04001package com.android.systemui.qs;
2
Amin Shaikha07a17b2018-02-23 16:02:52 -05003import android.animation.Animator;
4import android.animation.AnimatorListenerAdapter;
5import android.animation.AnimatorSet;
6import android.animation.ObjectAnimator;
7import android.animation.PropertyValuesHolder;
Jason Monkcaf37622015-08-18 12:33:50 -04008import android.content.Context;
Jason Monk6573ef22016-04-06 12:37:18 -04009import android.content.res.Configuration;
10import android.content.res.Resources;
Jason Monk51fb85a2016-03-28 14:06:04 -040011import android.support.v4.view.PagerAdapter;
12import android.support.v4.view.ViewPager;
Jason Monkcaf37622015-08-18 12:33:50 -040013import android.util.AttributeSet;
14import android.util.Log;
15import android.view.LayoutInflater;
Amin Shaikha07a17b2018-02-23 16:02:52 -050016import android.view.MotionEvent;
Jason Monkcaf37622015-08-18 12:33:50 -040017import android.view.View;
18import android.view.ViewGroup;
Amin Shaikha07a17b2018-02-23 16:02:52 -050019import android.view.animation.Interpolator;
20import android.view.animation.OvershootInterpolator;
21import android.widget.Scroller;
Jason Monkdf36aed2016-07-25 11:21:56 -040022
Jason Monkcaf37622015-08-18 12:33:50 -040023import com.android.systemui.R;
24import com.android.systemui.qs.QSPanel.QSTileLayout;
25import com.android.systemui.qs.QSPanel.TileRecord;
26
27import java.util.ArrayList;
Amin Shaikha07a17b2018-02-23 16:02:52 -050028import java.util.Set;
Jason Monkcaf37622015-08-18 12:33:50 -040029
30public class PagedTileLayout extends ViewPager implements QSTileLayout {
31
32 private static final boolean DEBUG = false;
33
34 private static final String TAG = "PagedTileLayout";
Amin Shaikha07a17b2018-02-23 16:02:52 -050035 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
Jason Monkcaf37622015-08-18 12:33:50 -040044
Amin Shaikh9978c552018-03-22 07:24:41 -040045 private final ArrayList<TileRecord> mTiles = new ArrayList<>();
46 private final ArrayList<TilePage> mPages = new ArrayList<>();
Jason Monkcaf37622015-08-18 12:33:50 -040047
Jason Monkcaf37622015-08-18 12:33:50 -040048 private PageIndicator mPageIndicator;
Rohan Shah3090e792018-04-12 00:01:00 -040049 private float mPageIndicatorPosition;
Jason Monkcaf37622015-08-18 12:33:50 -040050
51 private int mNumPages;
Jason Monk162011e2016-02-19 08:11:55 -050052 private PageListener mPageListener;
Jason Monkcaf37622015-08-18 12:33:50 -040053
Jason Monke5107a32016-05-31 15:40:58 -040054 private boolean mListening;
Amin Shaikha07a17b2018-02-23 16:02:52 -050055 private Scroller mScroller;
56
57 private AnimatorSet mBounceAnimatorSet;
58 private int mAnimatingToPage = -1;
Amin Shaikh4c9048c2018-04-20 11:27:46 -040059 private float mLastExpansion;
Jason Monke5107a32016-05-31 15:40:58 -040060
Jason Monkcaf37622015-08-18 12:33:50 -040061 public PagedTileLayout(Context context, AttributeSet attrs) {
62 super(context, attrs);
Amin Shaikha07a17b2018-02-23 16:02:52 -050063 mScroller = new Scroller(context, SCROLL_CUBIC);
Jason Monkcaf37622015-08-18 12:33:50 -040064 setAdapter(mAdapter);
Amin Shaikha07a17b2018-02-23 16:02:52 -050065 setOnPageChangeListener(mOnPageChangeListener);
66 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -040067 }
68
69 @Override
Jason Monk51fb85a2016-03-28 14:06:04 -040070 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
Jason Monke5107a32016-05-31 15:40:58 -040085 public void setListening(boolean listening) {
86 if (mListening == listening) return;
87 mListening = listening;
Amin Shaikh9978c552018-03-22 07:24:41 -040088 updateListening();
89 }
90
91 private void updateListening() {
92 for (TilePage tilePage : mPages) {
93 tilePage.setListening(tilePage.getParent() == null ? false : mListening);
Jason Monke5107a32016-05-31 15:40:58 -040094 }
95 }
96
Amin Shaikha07a17b2018-02-23 16:02:52 -050097 @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
Jason Monke5107a32016-05-31 15:40:58 -0400136 @Override
Jason Monkc5bdafb2016-02-25 16:24:21 -0500137 public boolean hasOverlappingRendering() {
138 return false;
139 }
140
141 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400142 protected void onFinishInflate() {
143 super.onFinishInflate();
Jason Monk32508852017-01-18 09:17:13 -0500144 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monke4e69302016-01-20 11:27:15 -0500145 .inflate(R.layout.qs_paged_page, this, false));
Jason Monkcaf37622015-08-18 12:33:50 -0400146 }
147
Jason Monk32508852017-01-18 09:17:13 -0500148 public void setPageIndicator(PageIndicator indicator) {
149 mPageIndicator = indicator;
Rohan Shah3090e792018-04-12 00:01:00 -0400150 mPageIndicator.setNumPages(mNumPages);
151 mPageIndicator.setLocation(mPageIndicatorPosition);
Jason Monk32508852017-01-18 09:17:13 -0500152 }
153
Jason Monkcaf37622015-08-18 12:33:50 -0400154 @Override
155 public int getOffsetTop(TileRecord tile) {
Jason Monkae5bd032016-03-02 14:38:31 -0500156 final ViewGroup parent = (ViewGroup) tile.tileView.getParent();
157 if (parent == null) return 0;
158 return parent.getTop() + getTop();
Jason Monkcaf37622015-08-18 12:33:50 -0400159 }
160
161 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400162 public void addTile(TileRecord tile) {
163 mTiles.add(tile);
Jason Monk9d02a432016-01-20 16:33:46 -0500164 postDistributeTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400165 }
166
167 @Override
168 public void removeTile(TileRecord tile) {
169 if (mTiles.remove(tile)) {
Jason Monk9d02a432016-01-20 16:33:46 -0500170 postDistributeTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400171 }
172 }
173
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400174 @Override
175 public void setExpansion(float expansion) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400176 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;
Rohan Shah2dbcb572018-05-25 10:51:22 -0700187
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);
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400192 for (int i = 0; i < mPages.size(); i++) {
193 mPages.get(i).setSelected(i == getCurrentItem() ? selected : false);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400194 }
Rohan Shah2dbcb572018-05-25 10:51:22 -0700195 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400196 }
197
Jason Monk162011e2016-02-19 08:11:55 -0500198 public void setPageListener(PageListener listener) {
199 mPageListener = listener;
200 }
201
Jason Monk9d02a432016-01-20 16:33:46 -0500202 private void postDistributeTiles() {
203 removeCallbacks(mDistribute);
204 post(mDistribute);
205 }
206
Jason Monkcaf37622015-08-18 12:33:50 -0400207 private void distributeTiles() {
208 if (DEBUG) Log.d(TAG, "Distributing tiles");
Jason Monkcaf37622015-08-18 12:33:50 -0400209 final int NP = mPages.size();
210 for (int i = 0; i < NP; i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500211 mPages.get(i).removeAllViews();
Jason Monkcaf37622015-08-18 12:33:50 -0400212 }
213 int index = 0;
214 final int NT = mTiles.size();
215 for (int i = 0; i < NT; i++) {
216 TileRecord tile = mTiles.get(i);
Jason Monkcaf37622015-08-18 12:33:50 -0400217 if (mPages.get(index).isFull()) {
218 if (++index == mPages.size()) {
Jason Monk8e73ef32016-05-04 11:36:46 -0400219 if (DEBUG) Log.d(TAG, "Adding page for "
220 + tile.tile.getClass().getSimpleName());
Jason Monk32508852017-01-18 09:17:13 -0500221 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monkcaf37622015-08-18 12:33:50 -0400222 .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;
Jason Monk8e73ef32016-05-04 11:36:46 -0400231 while (mPages.size() > mNumPages) {
232 mPages.remove(mPages.size() - 1);
233 }
234 if (DEBUG) Log.d(TAG, "Size: " + mNumPages);
Jason Monkcaf37622015-08-18 12:33:50 -0400235 mPageIndicator.setNumPages(mNumPages);
Jason Monk8e73ef32016-05-04 11:36:46 -0400236 setAdapter(mAdapter);
Jason Monkcaf37622015-08-18 12:33:50 -0400237 mAdapter.notifyDataSetChanged();
Jason Monk51fb85a2016-03-28 14:06:04 -0400238 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -0400239 }
240 }
241
242 @Override
Jason Monk9d02a432016-01-20 16:33:46 -0500243 public boolean updateResources() {
Rohan Shah3090e792018-04-12 00:01:00 -0400244 // 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
Jason Monk9d02a432016-01-20 16:33:46 -0500250 boolean changed = false;
Jason Monkcaf37622015-08-18 12:33:50 -0400251 for (int i = 0; i < mPages.size(); i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500252 changed |= mPages.get(i).updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400253 }
Jason Monk9d02a432016-01-20 16:33:46 -0500254 if (changed) {
255 distributeTiles();
256 }
257 return changed;
Jason Monkcaf37622015-08-18 12:33:50 -0400258 }
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 }
Jason Monkf13413e2017-02-15 15:49:32 -0500273 setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
Jason Monkcaf37622015-08-18 12:33:50 -0400274 }
275
Jason Monk9d02a432016-01-20 16:33:46 -0500276 private final Runnable mDistribute = new Runnable() {
277 @Override
278 public void run() {
279 distributeTiles();
280 }
281 };
282
Jason Monk8fb77872016-03-03 16:39:42 -0500283 public int getColumnCount() {
284 if (mPages.size() == 0) return 0;
285 return mPages.get(0).mColumns;
286 }
287
Amin Shaikha07a17b2018-02-23 16:02:52 -0500288 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) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400344 updateSelected();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500345 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;
Rohan Shah3090e792018-04-12 00:01:00 -0400356 mPageIndicatorPosition = position + positionOffset;
357 mPageIndicator.setLocation(mPageIndicatorPosition);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500358 if (mPageListener != null) {
359 mPageListener.onPageChanged(positionOffsetPixels == 0 &&
360 (isLayoutRtl() ? position == mPages.size() - 1 : position == 0));
361 }
362 }
363 };
364
Jason Monkcaf37622015-08-18 12:33:50 -0400365 public static class TilePage extends TileLayout {
Jason Monk94a1bf62015-10-20 08:43:36 -0700366 private int mMaxRows = 3;
Jason Monkcaf37622015-08-18 12:33:50 -0400367 public TilePage(Context context, AttributeSet attrs) {
368 super(context, attrs);
Jason Monkb9c00192015-10-07 11:45:33 -0400369 updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400370 }
371
Jason Monk9d02a432016-01-20 16:33:46 -0500372 @Override
373 public boolean updateResources() {
Jason Monk6573ef22016-04-06 12:37:18 -0400374 final int rows = getRows();
375 boolean changed = rows != mMaxRows;
376 if (changed) {
377 mMaxRows = rows;
378 requestLayout();
Jason Monk9d02a432016-01-20 16:33:46 -0500379 }
Jason Monk6573ef22016-04-06 12:37:18 -0400380 return super.updateResources() || changed;
381 }
382
383 private int getRows() {
Amin Shaikh78d834b2018-04-23 13:07:31 -0400384 return Math.max(1, getResources().getInteger(R.integer.quick_settings_num_rows));
Jason Monk9d02a432016-01-20 16:33:46 -0500385 }
386
Jason Monkbd6dbb02015-09-03 15:46:25 -0400387 public void setMaxRows(int maxRows) {
388 mMaxRows = maxRows;
389 }
390
Jason Monkbd6dbb02015-09-03 15:46:25 -0400391 public boolean isFull() {
Jason Monkcaf37622015-08-18 12:33:50 -0400392 return mRecords.size() >= mColumns * mMaxRows;
393 }
394 }
395
396 private final PagerAdapter mAdapter = new PagerAdapter() {
Amin Shaikh9978c552018-03-22 07:24:41 -0400397 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400398 public void destroyItem(ViewGroup container, int position, Object object) {
399 if (DEBUG) Log.d(TAG, "Destantiating " + position);
Jason Monkcaf37622015-08-18 12:33:50 -0400400 container.removeView((View) object);
Amin Shaikh9978c552018-03-22 07:24:41 -0400401 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400402 }
403
Amin Shaikh9978c552018-03-22 07:24:41 -0400404 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400405 public Object instantiateItem(ViewGroup container, int position) {
406 if (DEBUG) Log.d(TAG, "Instantiating " + position);
Jason Monk51fb85a2016-03-28 14:06:04 -0400407 if (isLayoutRtl()) {
408 position = mPages.size() - 1 - position;
409 }
Jason Monke4e69302016-01-20 11:27:15 -0500410 ViewGroup view = mPages.get(position);
Jason Monkcaf37622015-08-18 12:33:50 -0400411 container.addView(view);
Amin Shaikh9978c552018-03-22 07:24:41 -0400412 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400413 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 };
Jason Monk162011e2016-02-19 08:11:55 -0500426
427 public interface PageListener {
Jason Monk66eaf312016-02-25 12:29:29 -0500428 void onPageChanged(boolean isFirst);
Jason Monk162011e2016-02-19 08:11:55 -0500429 }
Jason Monkcaf37622015-08-18 12:33:50 -0400430}