blob: 123fca71deaf6128f9e1221351b1a967244d8f47 [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 Liutikasfd52c142018-04-17 09:50:46 -070011import androidx.viewpager.widget.PagerAdapter;
12import androidx.viewpager.widget.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;
Amin Shaikh4c9048c2018-04-20 11:27:46 -040058 private float mLastExpansion;
Jason Monke5107a32016-05-31 15:40:58 -040059
Jason Monkcaf37622015-08-18 12:33:50 -040060 public PagedTileLayout(Context context, AttributeSet attrs) {
61 super(context, attrs);
Amin Shaikha07a17b2018-02-23 16:02:52 -050062 mScroller = new Scroller(context, SCROLL_CUBIC);
Jason Monkcaf37622015-08-18 12:33:50 -040063 setAdapter(mAdapter);
Amin Shaikha07a17b2018-02-23 16:02:52 -050064 setOnPageChangeListener(mOnPageChangeListener);
65 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -040066 }
67
68 @Override
Jason Monk51fb85a2016-03-28 14:06:04 -040069 public void onRtlPropertiesChanged(int layoutDirection) {
70 super.onRtlPropertiesChanged(layoutDirection);
71 setAdapter(mAdapter);
72 setCurrentItem(0, false);
73 }
74
75 @Override
76 public void setCurrentItem(int item, boolean smoothScroll) {
77 if (isLayoutRtl()) {
78 item = mPages.size() - 1 - item;
79 }
80 super.setCurrentItem(item, smoothScroll);
81 }
82
83 @Override
Jason Monke5107a32016-05-31 15:40:58 -040084 public void setListening(boolean listening) {
85 if (mListening == listening) return;
86 mListening = listening;
Amin Shaikh9978c552018-03-22 07:24:41 -040087 updateListening();
88 }
89
90 private void updateListening() {
91 for (TilePage tilePage : mPages) {
92 tilePage.setListening(tilePage.getParent() == null ? false : mListening);
Jason Monke5107a32016-05-31 15:40:58 -040093 }
94 }
95
Amin Shaikha07a17b2018-02-23 16:02:52 -050096 @Override
Amin Shaikha07a17b2018-02-23 16:02:52 -050097 public void computeScroll() {
98 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
Amin Shaikh5484d0f2018-07-31 10:44:11 -040099 fakeDragBy(getScrollX() - mScroller.getCurrX());
Amin Shaikha07a17b2018-02-23 16:02:52 -0500100 // Keep on drawing until the animation has finished.
101 postInvalidateOnAnimation();
102 return;
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400103 } else if (isFakeDragging()) {
104 endFakeDrag();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500105 mBounceAnimatorSet.start();
106 setOffscreenPageLimit(1);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500107 }
108 super.computeScroll();
109 }
110
Jason Monke5107a32016-05-31 15:40:58 -0400111 @Override
Jason Monkc5bdafb2016-02-25 16:24:21 -0500112 public boolean hasOverlappingRendering() {
113 return false;
114 }
115
116 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400117 protected void onFinishInflate() {
118 super.onFinishInflate();
Jason Monk32508852017-01-18 09:17:13 -0500119 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monke4e69302016-01-20 11:27:15 -0500120 .inflate(R.layout.qs_paged_page, this, false));
Jason Monkcaf37622015-08-18 12:33:50 -0400121 }
122
Jason Monk32508852017-01-18 09:17:13 -0500123 public void setPageIndicator(PageIndicator indicator) {
124 mPageIndicator = indicator;
Rohan Shah3090e792018-04-12 00:01:00 -0400125 mPageIndicator.setNumPages(mNumPages);
126 mPageIndicator.setLocation(mPageIndicatorPosition);
Jason Monk32508852017-01-18 09:17:13 -0500127 }
128
Jason Monkcaf37622015-08-18 12:33:50 -0400129 @Override
130 public int getOffsetTop(TileRecord tile) {
Jason Monkae5bd032016-03-02 14:38:31 -0500131 final ViewGroup parent = (ViewGroup) tile.tileView.getParent();
132 if (parent == null) return 0;
133 return parent.getTop() + getTop();
Jason Monkcaf37622015-08-18 12:33:50 -0400134 }
135
136 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400137 public void addTile(TileRecord tile) {
138 mTiles.add(tile);
Jason Monk9d02a432016-01-20 16:33:46 -0500139 postDistributeTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400140 }
141
142 @Override
143 public void removeTile(TileRecord tile) {
144 if (mTiles.remove(tile)) {
Jason Monk9d02a432016-01-20 16:33:46 -0500145 postDistributeTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400146 }
147 }
148
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400149 @Override
150 public void setExpansion(float expansion) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400151 mLastExpansion = expansion;
152 updateSelected();
153 }
154
155 private void updateSelected() {
156 // Start the marquee when fully expanded and stop when fully collapsed. Leave as is for
157 // other expansion ratios since there is no way way to pause the marquee.
158 if (mLastExpansion > 0f && mLastExpansion < 1f) {
159 return;
160 }
161 boolean selected = mLastExpansion == 1f;
Rohan Shah2dbcb572018-05-25 10:51:22 -0700162
163 // Disable accessibility temporarily while we update selected state purely for the
164 // marquee. This will ensure that accessibility doesn't announce the TYPE_VIEW_SELECTED
165 // event on any of the children.
166 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
Amin Shaikh6f570742018-06-01 17:18:19 -0400167 int currentItem = isLayoutRtl() ? mPages.size() - 1 - getCurrentItem() : getCurrentItem();
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400168 for (int i = 0; i < mPages.size(); i++) {
Amin Shaikh6f570742018-06-01 17:18:19 -0400169 mPages.get(i).setSelected(i == currentItem ? selected : false);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400170 }
Rohan Shah2dbcb572018-05-25 10:51:22 -0700171 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400172 }
173
Jason Monk162011e2016-02-19 08:11:55 -0500174 public void setPageListener(PageListener listener) {
175 mPageListener = listener;
176 }
177
Jason Monk9d02a432016-01-20 16:33:46 -0500178 private void postDistributeTiles() {
179 removeCallbacks(mDistribute);
180 post(mDistribute);
181 }
182
Jason Monkcaf37622015-08-18 12:33:50 -0400183 private void distributeTiles() {
184 if (DEBUG) Log.d(TAG, "Distributing tiles");
Jason Monkcaf37622015-08-18 12:33:50 -0400185 final int NP = mPages.size();
186 for (int i = 0; i < NP; i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500187 mPages.get(i).removeAllViews();
Jason Monkcaf37622015-08-18 12:33:50 -0400188 }
189 int index = 0;
190 final int NT = mTiles.size();
191 for (int i = 0; i < NT; i++) {
192 TileRecord tile = mTiles.get(i);
Jason Monkcaf37622015-08-18 12:33:50 -0400193 if (mPages.get(index).isFull()) {
194 if (++index == mPages.size()) {
Jason Monk8e73ef32016-05-04 11:36:46 -0400195 if (DEBUG) Log.d(TAG, "Adding page for "
196 + tile.tile.getClass().getSimpleName());
Jason Monk32508852017-01-18 09:17:13 -0500197 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monkcaf37622015-08-18 12:33:50 -0400198 .inflate(R.layout.qs_paged_page, this, false));
199 }
200 }
201 if (DEBUG) Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
202 + index);
203 mPages.get(index).addTile(tile);
204 }
205 if (mNumPages != index + 1) {
206 mNumPages = index + 1;
Jason Monk8e73ef32016-05-04 11:36:46 -0400207 while (mPages.size() > mNumPages) {
208 mPages.remove(mPages.size() - 1);
209 }
210 if (DEBUG) Log.d(TAG, "Size: " + mNumPages);
Jason Monkcaf37622015-08-18 12:33:50 -0400211 mPageIndicator.setNumPages(mNumPages);
Jason Monk8e73ef32016-05-04 11:36:46 -0400212 setAdapter(mAdapter);
Jason Monkcaf37622015-08-18 12:33:50 -0400213 mAdapter.notifyDataSetChanged();
Jason Monk51fb85a2016-03-28 14:06:04 -0400214 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -0400215 }
216 }
217
218 @Override
Jason Monk9d02a432016-01-20 16:33:46 -0500219 public boolean updateResources() {
Rohan Shah3090e792018-04-12 00:01:00 -0400220 // Update bottom padding, useful for removing extra space once the panel page indicator is
221 // hidden.
222 setPadding(0, 0, 0,
223 getContext().getResources().getDimensionPixelSize(
224 R.dimen.qs_paged_tile_layout_padding_bottom));
225
Jason Monk9d02a432016-01-20 16:33:46 -0500226 boolean changed = false;
Jason Monkcaf37622015-08-18 12:33:50 -0400227 for (int i = 0; i < mPages.size(); i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500228 changed |= mPages.get(i).updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400229 }
Jason Monk9d02a432016-01-20 16:33:46 -0500230 if (changed) {
231 distributeTiles();
232 }
233 return changed;
Jason Monkcaf37622015-08-18 12:33:50 -0400234 }
235
236 @Override
237 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
238 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
239 // The ViewPager likes to eat all of the space, instead force it to wrap to the max height
240 // of the pages.
241 int maxHeight = 0;
242 final int N = getChildCount();
243 for (int i = 0; i < N; i++) {
244 int height = getChildAt(i).getMeasuredHeight();
245 if (height > maxHeight) {
246 maxHeight = height;
247 }
248 }
Jason Monkf13413e2017-02-15 15:49:32 -0500249 setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
Jason Monkcaf37622015-08-18 12:33:50 -0400250 }
251
Jason Monk9d02a432016-01-20 16:33:46 -0500252 private final Runnable mDistribute = new Runnable() {
253 @Override
254 public void run() {
255 distributeTiles();
256 }
257 };
258
Jason Monk8fb77872016-03-03 16:39:42 -0500259 public int getColumnCount() {
260 if (mPages.size() == 0) return 0;
261 return mPages.get(0).mColumns;
262 }
263
Fabian Kozynski802279f2018-09-07 13:44:54 -0400264 public int getNumVisibleTiles() {
265 if (mPages.size() == 0) return 0;
266 TilePage currentPage = mPages.get(getCurrentItem());
267 return currentPage.mRecords.size();
268 }
269
Amin Shaikha07a17b2018-02-23 16:02:52 -0500270 public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400271 if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0 || !beginFakeDrag()) {
Amin Shaikha07a17b2018-02-23 16:02:52 -0500272 // Do not start the reveal animation unless there are tiles to animate, multiple
273 // TilePages available and the user has not already started dragging.
274 return;
275 }
276
277 final int lastPageNumber = mPages.size() - 1;
278 final TilePage lastPage = mPages.get(lastPageNumber);
279 final ArrayList<Animator> bounceAnims = new ArrayList<>();
280 for (TileRecord tr : lastPage.mRecords) {
281 if (tileSpecs.contains(tr.tile.getTileSpec())) {
282 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
283 }
284 }
285
286 if (bounceAnims.isEmpty()) {
287 // All tileSpecs are on the first page. Nothing to do.
288 // TODO: potentially show a bounce animation for first page QS tiles
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400289 endFakeDrag();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500290 return;
291 }
292
293 mBounceAnimatorSet = new AnimatorSet();
294 mBounceAnimatorSet.playTogether(bounceAnims);
295 mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() {
296 @Override
297 public void onAnimationEnd(Animator animation) {
298 mBounceAnimatorSet = null;
299 postAnimation.run();
300 }
301 });
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400302 setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated.
303 int dx = getWidth() * lastPageNumber;
304 mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0,
305 REVEAL_SCROLL_DURATION_MILLIS);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500306 postInvalidateOnAnimation();
307 }
308
309 private static Animator setupBounceAnimator(View view, int ordinal) {
310 view.setAlpha(0f);
311 view.setScaleX(0f);
312 view.setScaleY(0f);
313 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
314 PropertyValuesHolder.ofFloat(View.ALPHA, 1),
315 PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
316 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
317 animator.setDuration(BOUNCE_ANIMATION_DURATION);
318 animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY);
319 animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
320 return animator;
321 }
322
323 private final ViewPager.OnPageChangeListener mOnPageChangeListener =
324 new ViewPager.SimpleOnPageChangeListener() {
325 @Override
326 public void onPageSelected(int position) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400327 updateSelected();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500328 if (mPageIndicator == null) return;
329 if (mPageListener != null) {
330 mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1
331 : position == 0);
332 }
333 }
334
335 @Override
336 public void onPageScrolled(int position, float positionOffset,
337 int positionOffsetPixels) {
338 if (mPageIndicator == null) return;
Rohan Shah3090e792018-04-12 00:01:00 -0400339 mPageIndicatorPosition = position + positionOffset;
340 mPageIndicator.setLocation(mPageIndicatorPosition);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500341 if (mPageListener != null) {
342 mPageListener.onPageChanged(positionOffsetPixels == 0 &&
343 (isLayoutRtl() ? position == mPages.size() - 1 : position == 0));
344 }
345 }
346 };
347
Jason Monkcaf37622015-08-18 12:33:50 -0400348 public static class TilePage extends TileLayout {
Jason Monk94a1bf62015-10-20 08:43:36 -0700349 private int mMaxRows = 3;
Jason Monkcaf37622015-08-18 12:33:50 -0400350 public TilePage(Context context, AttributeSet attrs) {
351 super(context, attrs);
Jason Monkb9c00192015-10-07 11:45:33 -0400352 updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400353 }
354
Jason Monk9d02a432016-01-20 16:33:46 -0500355 @Override
356 public boolean updateResources() {
Jason Monk6573ef22016-04-06 12:37:18 -0400357 final int rows = getRows();
358 boolean changed = rows != mMaxRows;
359 if (changed) {
360 mMaxRows = rows;
361 requestLayout();
Jason Monk9d02a432016-01-20 16:33:46 -0500362 }
Jason Monk6573ef22016-04-06 12:37:18 -0400363 return super.updateResources() || changed;
364 }
365
366 private int getRows() {
Amin Shaikh78d834b2018-04-23 13:07:31 -0400367 return Math.max(1, getResources().getInteger(R.integer.quick_settings_num_rows));
Jason Monk9d02a432016-01-20 16:33:46 -0500368 }
369
Jason Monkbd6dbb02015-09-03 15:46:25 -0400370 public void setMaxRows(int maxRows) {
371 mMaxRows = maxRows;
372 }
373
Jason Monkbd6dbb02015-09-03 15:46:25 -0400374 public boolean isFull() {
Jason Monkcaf37622015-08-18 12:33:50 -0400375 return mRecords.size() >= mColumns * mMaxRows;
376 }
377 }
378
379 private final PagerAdapter mAdapter = new PagerAdapter() {
Amin Shaikh9978c552018-03-22 07:24:41 -0400380 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400381 public void destroyItem(ViewGroup container, int position, Object object) {
382 if (DEBUG) Log.d(TAG, "Destantiating " + position);
Jason Monkcaf37622015-08-18 12:33:50 -0400383 container.removeView((View) object);
Amin Shaikh9978c552018-03-22 07:24:41 -0400384 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400385 }
386
Amin Shaikh9978c552018-03-22 07:24:41 -0400387 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400388 public Object instantiateItem(ViewGroup container, int position) {
389 if (DEBUG) Log.d(TAG, "Instantiating " + position);
Jason Monk51fb85a2016-03-28 14:06:04 -0400390 if (isLayoutRtl()) {
391 position = mPages.size() - 1 - position;
392 }
Jason Monke4e69302016-01-20 11:27:15 -0500393 ViewGroup view = mPages.get(position);
Amin Shaikh53bc0832018-09-17 15:06:18 -0400394 if (view.getParent() != null) {
395 container.removeView(view);
396 }
Jason Monkcaf37622015-08-18 12:33:50 -0400397 container.addView(view);
Amin Shaikh9978c552018-03-22 07:24:41 -0400398 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400399 return view;
400 }
401
402 @Override
403 public int getCount() {
404 return mNumPages;
405 }
406
407 @Override
408 public boolean isViewFromObject(View view, Object object) {
409 return view == object;
410 }
411 };
Jason Monk162011e2016-02-19 08:11:55 -0500412
413 public interface PageListener {
Jason Monk66eaf312016-02-25 12:29:29 -0500414 void onPageChanged(boolean isFirst);
Jason Monk162011e2016-02-19 08:11:55 -0500415 }
Jason Monkcaf37622015-08-18 12:33:50 -0400416}