blob: fdddefe39a8e7521ae259469f4cf326a0386b138 [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;
Aurimas Liutikasd3667712018-04-17 09:50:46 -070011import androidx.viewpager.widget.PagerAdapter;
12import androidx.viewpager.widget.ViewPager;
Fabian Kozynski4ba51732018-10-10 12:52:35 -040013import android.graphics.Rect;
Jason Monkcaf37622015-08-18 12:33:50 -040014import android.util.AttributeSet;
15import android.util.Log;
16import android.view.LayoutInflater;
Amin Shaikha07a17b2018-02-23 16:02:52 -050017import android.view.MotionEvent;
Jason Monkcaf37622015-08-18 12:33:50 -040018import android.view.View;
19import android.view.ViewGroup;
Amin Shaikha07a17b2018-02-23 16:02:52 -050020import android.view.animation.Interpolator;
21import android.view.animation.OvershootInterpolator;
22import android.widget.Scroller;
Jason Monkdf36aed2016-07-25 11:21:56 -040023
Jason Monkcaf37622015-08-18 12:33:50 -040024import com.android.systemui.R;
25import com.android.systemui.qs.QSPanel.QSTileLayout;
26import com.android.systemui.qs.QSPanel.TileRecord;
27
28import java.util.ArrayList;
Amin Shaikha07a17b2018-02-23 16:02:52 -050029import java.util.Set;
Jason Monkcaf37622015-08-18 12:33:50 -040030
31public class PagedTileLayout extends ViewPager implements QSTileLayout {
32
33 private static final boolean DEBUG = false;
34
35 private static final String TAG = "PagedTileLayout";
Amin Shaikha07a17b2018-02-23 16:02:52 -050036 private static final int REVEAL_SCROLL_DURATION_MILLIS = 750;
37 private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
38 private static final long BOUNCE_ANIMATION_DURATION = 450L;
39 private static final int TILE_ANIMATION_STAGGER_DELAY = 85;
40 private static final Interpolator SCROLL_CUBIC = (t) -> {
41 t -= 1.0f;
42 return t * t * t + 1.0f;
43 };
44
Jason Monkcaf37622015-08-18 12:33:50 -040045
Amin Shaikh9978c552018-03-22 07:24:41 -040046 private final ArrayList<TileRecord> mTiles = new ArrayList<>();
47 private final ArrayList<TilePage> mPages = new ArrayList<>();
Jason Monkcaf37622015-08-18 12:33:50 -040048
Jason Monkcaf37622015-08-18 12:33:50 -040049 private PageIndicator mPageIndicator;
Rohan Shah3090e792018-04-12 00:01:00 -040050 private float mPageIndicatorPosition;
Jason Monkcaf37622015-08-18 12:33:50 -040051
52 private int mNumPages;
Jason Monk162011e2016-02-19 08:11:55 -050053 private PageListener mPageListener;
Jason Monkcaf37622015-08-18 12:33:50 -040054
Jason Monke5107a32016-05-31 15:40:58 -040055 private boolean mListening;
Amin Shaikha07a17b2018-02-23 16:02:52 -050056 private Scroller mScroller;
57
58 private AnimatorSet mBounceAnimatorSet;
59 private int mAnimatingToPage = -1;
Amin Shaikh4c9048c2018-04-20 11:27:46 -040060 private float mLastExpansion;
Fabian Kozynski4ba51732018-10-10 12:52:35 -040061 private int mHorizontalClipBounds;
Jason Monke5107a32016-05-31 15:40:58 -040062
Jason Monkcaf37622015-08-18 12:33:50 -040063 public PagedTileLayout(Context context, AttributeSet attrs) {
64 super(context, attrs);
Amin Shaikha07a17b2018-02-23 16:02:52 -050065 mScroller = new Scroller(context, SCROLL_CUBIC);
Jason Monkcaf37622015-08-18 12:33:50 -040066 setAdapter(mAdapter);
Amin Shaikha07a17b2018-02-23 16:02:52 -050067 setOnPageChangeListener(mOnPageChangeListener);
68 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -040069 }
70
71 @Override
Jason Monk51fb85a2016-03-28 14:06:04 -040072 public void onRtlPropertiesChanged(int layoutDirection) {
73 super.onRtlPropertiesChanged(layoutDirection);
74 setAdapter(mAdapter);
75 setCurrentItem(0, false);
76 }
77
78 @Override
79 public void setCurrentItem(int item, boolean smoothScroll) {
80 if (isLayoutRtl()) {
81 item = mPages.size() - 1 - item;
82 }
83 super.setCurrentItem(item, smoothScroll);
84 }
85
86 @Override
Jason Monke5107a32016-05-31 15:40:58 -040087 public void setListening(boolean listening) {
88 if (mListening == listening) return;
89 mListening = listening;
Amin Shaikh9978c552018-03-22 07:24:41 -040090 updateListening();
91 }
92
93 private void updateListening() {
94 for (TilePage tilePage : mPages) {
95 tilePage.setListening(tilePage.getParent() == null ? false : mListening);
Jason Monke5107a32016-05-31 15:40:58 -040096 }
97 }
98
Amin Shaikha07a17b2018-02-23 16:02:52 -050099 @Override
100 public boolean onInterceptTouchEvent(MotionEvent ev) {
101 // Suppress all touch event during reveal animation.
102 if (mAnimatingToPage != -1) {
103 return true;
104 }
105 return super.onInterceptTouchEvent(ev);
106 }
107
108 @Override
109 public boolean onTouchEvent(MotionEvent ev) {
110 // Suppress all touch event during reveal animation.
111 if (mAnimatingToPage != -1) {
112 return true;
113 }
114 return super.onTouchEvent(ev);
115 }
116
117 @Override
118 public void computeScroll() {
119 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
120 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
121 float pageFraction = (float) getScrollX() / getWidth();
122 int position = (int) pageFraction;
123 float positionOffset = pageFraction - position;
124 mOnPageChangeListener.onPageScrolled(position, positionOffset, getScrollX());
125 // Keep on drawing until the animation has finished.
126 postInvalidateOnAnimation();
127 return;
128 }
129 if (mAnimatingToPage != -1) {
130 setCurrentItem(mAnimatingToPage, true);
131 mBounceAnimatorSet.start();
132 setOffscreenPageLimit(1);
133 mAnimatingToPage = -1;
134 }
135 super.computeScroll();
136 }
137
Jason Monke5107a32016-05-31 15:40:58 -0400138 @Override
Jason Monkc5bdafb2016-02-25 16:24:21 -0500139 public boolean hasOverlappingRendering() {
140 return false;
141 }
142
143 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400144 protected void onFinishInflate() {
145 super.onFinishInflate();
Jason Monk32508852017-01-18 09:17:13 -0500146 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monke4e69302016-01-20 11:27:15 -0500147 .inflate(R.layout.qs_paged_page, this, false));
Jason Monkcaf37622015-08-18 12:33:50 -0400148 }
149
Jason Monk32508852017-01-18 09:17:13 -0500150 public void setPageIndicator(PageIndicator indicator) {
151 mPageIndicator = indicator;
Rohan Shah3090e792018-04-12 00:01:00 -0400152 mPageIndicator.setNumPages(mNumPages);
153 mPageIndicator.setLocation(mPageIndicatorPosition);
Jason Monk32508852017-01-18 09:17:13 -0500154 }
155
Jason Monkcaf37622015-08-18 12:33:50 -0400156 @Override
157 public int getOffsetTop(TileRecord tile) {
Jason Monkae5bd032016-03-02 14:38:31 -0500158 final ViewGroup parent = (ViewGroup) tile.tileView.getParent();
159 if (parent == null) return 0;
160 return parent.getTop() + getTop();
Jason Monkcaf37622015-08-18 12:33:50 -0400161 }
162
163 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400164 public void addTile(TileRecord tile) {
165 mTiles.add(tile);
Jason Monk9d02a432016-01-20 16:33:46 -0500166 postDistributeTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400167 }
168
169 @Override
170 public void removeTile(TileRecord tile) {
171 if (mTiles.remove(tile)) {
Jason Monk9d02a432016-01-20 16:33:46 -0500172 postDistributeTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400173 }
174 }
175
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400176 @Override
177 public void setExpansion(float expansion) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400178 mLastExpansion = expansion;
179 updateSelected();
180 }
181
182 private void updateSelected() {
183 // Start the marquee when fully expanded and stop when fully collapsed. Leave as is for
184 // other expansion ratios since there is no way way to pause the marquee.
185 if (mLastExpansion > 0f && mLastExpansion < 1f) {
186 return;
187 }
188 boolean selected = mLastExpansion == 1f;
Rohan Shah2dbcb572018-05-25 10:51:22 -0700189
190 // Disable accessibility temporarily while we update selected state purely for the
191 // marquee. This will ensure that accessibility doesn't announce the TYPE_VIEW_SELECTED
192 // event on any of the children.
193 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400194 for (int i = 0; i < mPages.size(); i++) {
195 mPages.get(i).setSelected(i == getCurrentItem() ? selected : false);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400196 }
Rohan Shah2dbcb572018-05-25 10:51:22 -0700197 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400198 }
199
Jason Monk162011e2016-02-19 08:11:55 -0500200 public void setPageListener(PageListener listener) {
201 mPageListener = listener;
202 }
203
Jason Monk9d02a432016-01-20 16:33:46 -0500204 private void postDistributeTiles() {
205 removeCallbacks(mDistribute);
206 post(mDistribute);
207 }
208
Jason Monkcaf37622015-08-18 12:33:50 -0400209 private void distributeTiles() {
210 if (DEBUG) Log.d(TAG, "Distributing tiles");
Jason Monkcaf37622015-08-18 12:33:50 -0400211 final int NP = mPages.size();
212 for (int i = 0; i < NP; i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500213 mPages.get(i).removeAllViews();
Jason Monkcaf37622015-08-18 12:33:50 -0400214 }
215 int index = 0;
216 final int NT = mTiles.size();
217 for (int i = 0; i < NT; i++) {
218 TileRecord tile = mTiles.get(i);
Jason Monkcaf37622015-08-18 12:33:50 -0400219 if (mPages.get(index).isFull()) {
220 if (++index == mPages.size()) {
Jason Monk8e73ef32016-05-04 11:36:46 -0400221 if (DEBUG) Log.d(TAG, "Adding page for "
222 + tile.tile.getClass().getSimpleName());
Jason Monk32508852017-01-18 09:17:13 -0500223 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monkcaf37622015-08-18 12:33:50 -0400224 .inflate(R.layout.qs_paged_page, this, false));
225 }
226 }
227 if (DEBUG) Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
228 + index);
229 mPages.get(index).addTile(tile);
230 }
231 if (mNumPages != index + 1) {
232 mNumPages = index + 1;
Jason Monk8e73ef32016-05-04 11:36:46 -0400233 while (mPages.size() > mNumPages) {
234 mPages.remove(mPages.size() - 1);
235 }
236 if (DEBUG) Log.d(TAG, "Size: " + mNumPages);
Jason Monkcaf37622015-08-18 12:33:50 -0400237 mPageIndicator.setNumPages(mNumPages);
Jason Monk8e73ef32016-05-04 11:36:46 -0400238 setAdapter(mAdapter);
Jason Monkcaf37622015-08-18 12:33:50 -0400239 mAdapter.notifyDataSetChanged();
Jason Monk51fb85a2016-03-28 14:06:04 -0400240 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -0400241 }
242 }
243
244 @Override
Jason Monk9d02a432016-01-20 16:33:46 -0500245 public boolean updateResources() {
Rohan Shah3090e792018-04-12 00:01:00 -0400246 // Update bottom padding, useful for removing extra space once the panel page indicator is
247 // hidden.
Fabian Kozynski4ba51732018-10-10 12:52:35 -0400248 mHorizontalClipBounds = getContext().getResources().getDimensionPixelSize(
249 R.dimen.notification_side_paddings);
Rohan Shah3090e792018-04-12 00:01:00 -0400250 setPadding(0, 0, 0,
251 getContext().getResources().getDimensionPixelSize(
252 R.dimen.qs_paged_tile_layout_padding_bottom));
253
Jason Monk9d02a432016-01-20 16:33:46 -0500254 boolean changed = false;
Jason Monkcaf37622015-08-18 12:33:50 -0400255 for (int i = 0; i < mPages.size(); i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500256 changed |= mPages.get(i).updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400257 }
Jason Monk9d02a432016-01-20 16:33:46 -0500258 if (changed) {
259 distributeTiles();
260 }
261 return changed;
Jason Monkcaf37622015-08-18 12:33:50 -0400262 }
263
264 @Override
265 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
266 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
267 // The ViewPager likes to eat all of the space, instead force it to wrap to the max height
268 // of the pages.
269 int maxHeight = 0;
270 final int N = getChildCount();
271 for (int i = 0; i < N; i++) {
272 int height = getChildAt(i).getMeasuredHeight();
273 if (height > maxHeight) {
274 maxHeight = height;
275 }
276 }
Jason Monkf13413e2017-02-15 15:49:32 -0500277 setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
Jason Monkcaf37622015-08-18 12:33:50 -0400278 }
279
Fabian Kozynski4ba51732018-10-10 12:52:35 -0400280 @Override
281 protected void onLayout(boolean changed, int l, int t, int r, int b) {
282 super.onLayout(changed, l, t, r, b);
283 Rect clipBounds = new Rect(mHorizontalClipBounds, 0,
284 r - l - mHorizontalClipBounds, b - t);
285 setClipBounds(clipBounds);
286 }
287
288
Jason Monk9d02a432016-01-20 16:33:46 -0500289 private final Runnable mDistribute = new Runnable() {
290 @Override
291 public void run() {
292 distributeTiles();
293 }
294 };
295
Jason Monk8fb77872016-03-03 16:39:42 -0500296 public int getColumnCount() {
297 if (mPages.size() == 0) return 0;
298 return mPages.get(0).mColumns;
299 }
300
Amin Shaikha07a17b2018-02-23 16:02:52 -0500301 public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
302 if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0) {
303 // Do not start the reveal animation unless there are tiles to animate, multiple
304 // TilePages available and the user has not already started dragging.
305 return;
306 }
307
308 final int lastPageNumber = mPages.size() - 1;
309 final TilePage lastPage = mPages.get(lastPageNumber);
310 final ArrayList<Animator> bounceAnims = new ArrayList<>();
311 for (TileRecord tr : lastPage.mRecords) {
312 if (tileSpecs.contains(tr.tile.getTileSpec())) {
313 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
314 }
315 }
316
317 if (bounceAnims.isEmpty()) {
318 // All tileSpecs are on the first page. Nothing to do.
319 // TODO: potentially show a bounce animation for first page QS tiles
320 return;
321 }
322
323 mBounceAnimatorSet = new AnimatorSet();
324 mBounceAnimatorSet.playTogether(bounceAnims);
325 mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() {
326 @Override
327 public void onAnimationEnd(Animator animation) {
328 mBounceAnimatorSet = null;
329 postAnimation.run();
330 }
331 });
332 mAnimatingToPage = lastPageNumber;
333 setOffscreenPageLimit(mAnimatingToPage); // Ensure the page to reveal has been inflated.
334 mScroller.startScroll(getScrollX(), getScrollY(), getWidth() * mAnimatingToPage, 0,
335 REVEAL_SCROLL_DURATION_MILLIS);
336 postInvalidateOnAnimation();
337 }
338
339 private static Animator setupBounceAnimator(View view, int ordinal) {
340 view.setAlpha(0f);
341 view.setScaleX(0f);
342 view.setScaleY(0f);
343 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
344 PropertyValuesHolder.ofFloat(View.ALPHA, 1),
345 PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
346 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
347 animator.setDuration(BOUNCE_ANIMATION_DURATION);
348 animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY);
349 animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
350 return animator;
351 }
352
353 private final ViewPager.OnPageChangeListener mOnPageChangeListener =
354 new ViewPager.SimpleOnPageChangeListener() {
355 @Override
356 public void onPageSelected(int position) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400357 updateSelected();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500358 if (mPageIndicator == null) return;
359 if (mPageListener != null) {
360 mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1
361 : position == 0);
362 }
363 }
364
365 @Override
366 public void onPageScrolled(int position, float positionOffset,
367 int positionOffsetPixels) {
368 if (mPageIndicator == null) return;
Rohan Shah3090e792018-04-12 00:01:00 -0400369 mPageIndicatorPosition = position + positionOffset;
370 mPageIndicator.setLocation(mPageIndicatorPosition);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500371 if (mPageListener != null) {
372 mPageListener.onPageChanged(positionOffsetPixels == 0 &&
373 (isLayoutRtl() ? position == mPages.size() - 1 : position == 0));
374 }
375 }
376 };
377
Jason Monkcaf37622015-08-18 12:33:50 -0400378 public static class TilePage extends TileLayout {
Jason Monk94a1bf62015-10-20 08:43:36 -0700379 private int mMaxRows = 3;
Jason Monkcaf37622015-08-18 12:33:50 -0400380 public TilePage(Context context, AttributeSet attrs) {
381 super(context, attrs);
Jason Monkb9c00192015-10-07 11:45:33 -0400382 updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400383 }
384
Jason Monk9d02a432016-01-20 16:33:46 -0500385 @Override
386 public boolean updateResources() {
Jason Monk6573ef22016-04-06 12:37:18 -0400387 final int rows = getRows();
388 boolean changed = rows != mMaxRows;
389 if (changed) {
390 mMaxRows = rows;
391 requestLayout();
Jason Monk9d02a432016-01-20 16:33:46 -0500392 }
Jason Monk6573ef22016-04-06 12:37:18 -0400393 return super.updateResources() || changed;
394 }
395
396 private int getRows() {
Amin Shaikh78d834b2018-04-23 13:07:31 -0400397 return Math.max(1, getResources().getInteger(R.integer.quick_settings_num_rows));
Jason Monk9d02a432016-01-20 16:33:46 -0500398 }
399
Jason Monkbd6dbb02015-09-03 15:46:25 -0400400 public void setMaxRows(int maxRows) {
401 mMaxRows = maxRows;
402 }
403
Jason Monkbd6dbb02015-09-03 15:46:25 -0400404 public boolean isFull() {
Jason Monkcaf37622015-08-18 12:33:50 -0400405 return mRecords.size() >= mColumns * mMaxRows;
406 }
407 }
408
409 private final PagerAdapter mAdapter = new PagerAdapter() {
Amin Shaikh9978c552018-03-22 07:24:41 -0400410 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400411 public void destroyItem(ViewGroup container, int position, Object object) {
412 if (DEBUG) Log.d(TAG, "Destantiating " + position);
Jason Monkcaf37622015-08-18 12:33:50 -0400413 container.removeView((View) object);
Amin Shaikh9978c552018-03-22 07:24:41 -0400414 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400415 }
416
Amin Shaikh9978c552018-03-22 07:24:41 -0400417 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400418 public Object instantiateItem(ViewGroup container, int position) {
419 if (DEBUG) Log.d(TAG, "Instantiating " + position);
Jason Monk51fb85a2016-03-28 14:06:04 -0400420 if (isLayoutRtl()) {
421 position = mPages.size() - 1 - position;
422 }
Jason Monke4e69302016-01-20 11:27:15 -0500423 ViewGroup view = mPages.get(position);
Jason Monkcaf37622015-08-18 12:33:50 -0400424 container.addView(view);
Amin Shaikh9978c552018-03-22 07:24:41 -0400425 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400426 return view;
427 }
428
429 @Override
430 public int getCount() {
431 return mNumPages;
432 }
433
434 @Override
435 public boolean isViewFromObject(View view, Object object) {
436 return view == object;
437 }
438 };
Jason Monk162011e2016-02-19 08:11:55 -0500439
440 public interface PageListener {
Jason Monk66eaf312016-02-25 12:29:29 -0500441 void onPageChanged(boolean isFirst);
Jason Monk162011e2016-02-19 08:11:55 -0500442 }
Jason Monkcaf37622015-08-18 12:33:50 -0400443}