blob: f13f4899baed549ccbfabfdba424df7fd206d6d6 [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;
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 Shaikh6f570742018-06-01 17:18:19 -0400192 int currentItem = isLayoutRtl() ? mPages.size() - 1 - getCurrentItem() : getCurrentItem();
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400193 for (int i = 0; i < mPages.size(); i++) {
Amin Shaikh6f570742018-06-01 17:18:19 -0400194 mPages.get(i).setSelected(i == currentItem ? selected : false);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400195 }
Rohan Shah2dbcb572018-05-25 10:51:22 -0700196 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400197 }
198
Jason Monk162011e2016-02-19 08:11:55 -0500199 public void setPageListener(PageListener listener) {
200 mPageListener = listener;
201 }
202
Jason Monk9d02a432016-01-20 16:33:46 -0500203 private void postDistributeTiles() {
204 removeCallbacks(mDistribute);
205 post(mDistribute);
206 }
207
Jason Monkcaf37622015-08-18 12:33:50 -0400208 private void distributeTiles() {
209 if (DEBUG) Log.d(TAG, "Distributing tiles");
Jason Monkcaf37622015-08-18 12:33:50 -0400210 final int NP = mPages.size();
211 for (int i = 0; i < NP; i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500212 mPages.get(i).removeAllViews();
Jason Monkcaf37622015-08-18 12:33:50 -0400213 }
214 int index = 0;
215 final int NT = mTiles.size();
216 for (int i = 0; i < NT; i++) {
217 TileRecord tile = mTiles.get(i);
Jason Monkcaf37622015-08-18 12:33:50 -0400218 if (mPages.get(index).isFull()) {
219 if (++index == mPages.size()) {
Jason Monk8e73ef32016-05-04 11:36:46 -0400220 if (DEBUG) Log.d(TAG, "Adding page for "
221 + tile.tile.getClass().getSimpleName());
Jason Monk32508852017-01-18 09:17:13 -0500222 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monkcaf37622015-08-18 12:33:50 -0400223 .inflate(R.layout.qs_paged_page, this, false));
224 }
225 }
226 if (DEBUG) Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
227 + index);
228 mPages.get(index).addTile(tile);
229 }
230 if (mNumPages != index + 1) {
231 mNumPages = index + 1;
Jason Monk8e73ef32016-05-04 11:36:46 -0400232 while (mPages.size() > mNumPages) {
233 mPages.remove(mPages.size() - 1);
234 }
235 if (DEBUG) Log.d(TAG, "Size: " + mNumPages);
Jason Monkcaf37622015-08-18 12:33:50 -0400236 mPageIndicator.setNumPages(mNumPages);
Jason Monk8e73ef32016-05-04 11:36:46 -0400237 setAdapter(mAdapter);
Jason Monkcaf37622015-08-18 12:33:50 -0400238 mAdapter.notifyDataSetChanged();
Jason Monk51fb85a2016-03-28 14:06:04 -0400239 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -0400240 }
241 }
242
243 @Override
Jason Monk9d02a432016-01-20 16:33:46 -0500244 public boolean updateResources() {
Rohan Shah3090e792018-04-12 00:01:00 -0400245 // Update bottom padding, useful for removing extra space once the panel page indicator is
246 // hidden.
247 setPadding(0, 0, 0,
248 getContext().getResources().getDimensionPixelSize(
249 R.dimen.qs_paged_tile_layout_padding_bottom));
250
Jason Monk9d02a432016-01-20 16:33:46 -0500251 boolean changed = false;
Jason Monkcaf37622015-08-18 12:33:50 -0400252 for (int i = 0; i < mPages.size(); i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500253 changed |= mPages.get(i).updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400254 }
Jason Monk9d02a432016-01-20 16:33:46 -0500255 if (changed) {
256 distributeTiles();
257 }
258 return changed;
Jason Monkcaf37622015-08-18 12:33:50 -0400259 }
260
261 @Override
262 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
263 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
264 // The ViewPager likes to eat all of the space, instead force it to wrap to the max height
265 // of the pages.
266 int maxHeight = 0;
267 final int N = getChildCount();
268 for (int i = 0; i < N; i++) {
269 int height = getChildAt(i).getMeasuredHeight();
270 if (height > maxHeight) {
271 maxHeight = height;
272 }
273 }
Jason Monkf13413e2017-02-15 15:49:32 -0500274 setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
Jason Monkcaf37622015-08-18 12:33:50 -0400275 }
276
Jason Monk9d02a432016-01-20 16:33:46 -0500277 private final Runnable mDistribute = new Runnable() {
278 @Override
279 public void run() {
280 distributeTiles();
281 }
282 };
283
Jason Monk8fb77872016-03-03 16:39:42 -0500284 public int getColumnCount() {
285 if (mPages.size() == 0) return 0;
286 return mPages.get(0).mColumns;
287 }
288
Amin Shaikha07a17b2018-02-23 16:02:52 -0500289 public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
290 if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0) {
291 // Do not start the reveal animation unless there are tiles to animate, multiple
292 // TilePages available and the user has not already started dragging.
293 return;
294 }
295
296 final int lastPageNumber = mPages.size() - 1;
297 final TilePage lastPage = mPages.get(lastPageNumber);
298 final ArrayList<Animator> bounceAnims = new ArrayList<>();
299 for (TileRecord tr : lastPage.mRecords) {
300 if (tileSpecs.contains(tr.tile.getTileSpec())) {
301 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
302 }
303 }
304
305 if (bounceAnims.isEmpty()) {
306 // All tileSpecs are on the first page. Nothing to do.
307 // TODO: potentially show a bounce animation for first page QS tiles
308 return;
309 }
310
311 mBounceAnimatorSet = new AnimatorSet();
312 mBounceAnimatorSet.playTogether(bounceAnims);
313 mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() {
314 @Override
315 public void onAnimationEnd(Animator animation) {
316 mBounceAnimatorSet = null;
317 postAnimation.run();
318 }
319 });
320 mAnimatingToPage = lastPageNumber;
321 setOffscreenPageLimit(mAnimatingToPage); // Ensure the page to reveal has been inflated.
322 mScroller.startScroll(getScrollX(), getScrollY(), getWidth() * mAnimatingToPage, 0,
323 REVEAL_SCROLL_DURATION_MILLIS);
324 postInvalidateOnAnimation();
325 }
326
327 private static Animator setupBounceAnimator(View view, int ordinal) {
328 view.setAlpha(0f);
329 view.setScaleX(0f);
330 view.setScaleY(0f);
331 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
332 PropertyValuesHolder.ofFloat(View.ALPHA, 1),
333 PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
334 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
335 animator.setDuration(BOUNCE_ANIMATION_DURATION);
336 animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY);
337 animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
338 return animator;
339 }
340
341 private final ViewPager.OnPageChangeListener mOnPageChangeListener =
342 new ViewPager.SimpleOnPageChangeListener() {
343 @Override
344 public void onPageSelected(int position) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400345 updateSelected();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500346 if (mPageIndicator == null) return;
347 if (mPageListener != null) {
348 mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1
349 : position == 0);
350 }
351 }
352
353 @Override
354 public void onPageScrolled(int position, float positionOffset,
355 int positionOffsetPixels) {
356 if (mPageIndicator == null) return;
Rohan Shah3090e792018-04-12 00:01:00 -0400357 mPageIndicatorPosition = position + positionOffset;
358 mPageIndicator.setLocation(mPageIndicatorPosition);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500359 if (mPageListener != null) {
360 mPageListener.onPageChanged(positionOffsetPixels == 0 &&
361 (isLayoutRtl() ? position == mPages.size() - 1 : position == 0));
362 }
363 }
364 };
365
Jason Monkcaf37622015-08-18 12:33:50 -0400366 public static class TilePage extends TileLayout {
Jason Monk94a1bf62015-10-20 08:43:36 -0700367 private int mMaxRows = 3;
Jason Monkcaf37622015-08-18 12:33:50 -0400368 public TilePage(Context context, AttributeSet attrs) {
369 super(context, attrs);
Jason Monkb9c00192015-10-07 11:45:33 -0400370 updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400371 }
372
Jason Monk9d02a432016-01-20 16:33:46 -0500373 @Override
374 public boolean updateResources() {
Jason Monk6573ef22016-04-06 12:37:18 -0400375 final int rows = getRows();
376 boolean changed = rows != mMaxRows;
377 if (changed) {
378 mMaxRows = rows;
379 requestLayout();
Jason Monk9d02a432016-01-20 16:33:46 -0500380 }
Jason Monk6573ef22016-04-06 12:37:18 -0400381 return super.updateResources() || changed;
382 }
383
384 private int getRows() {
Amin Shaikh78d834b2018-04-23 13:07:31 -0400385 return Math.max(1, getResources().getInteger(R.integer.quick_settings_num_rows));
Jason Monk9d02a432016-01-20 16:33:46 -0500386 }
387
Jason Monkbd6dbb02015-09-03 15:46:25 -0400388 public void setMaxRows(int maxRows) {
389 mMaxRows = maxRows;
390 }
391
Jason Monkbd6dbb02015-09-03 15:46:25 -0400392 public boolean isFull() {
Jason Monkcaf37622015-08-18 12:33:50 -0400393 return mRecords.size() >= mColumns * mMaxRows;
394 }
395 }
396
397 private final PagerAdapter mAdapter = new PagerAdapter() {
Amin Shaikh9978c552018-03-22 07:24:41 -0400398 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400399 public void destroyItem(ViewGroup container, int position, Object object) {
400 if (DEBUG) Log.d(TAG, "Destantiating " + position);
Jason Monkcaf37622015-08-18 12:33:50 -0400401 container.removeView((View) object);
Amin Shaikh9978c552018-03-22 07:24:41 -0400402 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400403 }
404
Amin Shaikh9978c552018-03-22 07:24:41 -0400405 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400406 public Object instantiateItem(ViewGroup container, int position) {
407 if (DEBUG) Log.d(TAG, "Instantiating " + position);
Jason Monk51fb85a2016-03-28 14:06:04 -0400408 if (isLayoutRtl()) {
409 position = mPages.size() - 1 - position;
410 }
Jason Monke4e69302016-01-20 11:27:15 -0500411 ViewGroup view = mPages.get(position);
Jason Monkcaf37622015-08-18 12:33:50 -0400412 container.addView(view);
Amin Shaikh9978c552018-03-22 07:24:41 -0400413 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400414 return view;
415 }
416
417 @Override
418 public int getCount() {
419 return mNumPages;
420 }
421
422 @Override
423 public boolean isViewFromObject(View view, Object object) {
424 return view == object;
425 }
426 };
Jason Monk162011e2016-02-19 08:11:55 -0500427
428 public interface PageListener {
Jason Monk66eaf312016-02-25 12:29:29 -0500429 void onPageChanged(boolean isFirst);
Jason Monk162011e2016-02-19 08:11:55 -0500430 }
Jason Monkcaf37622015-08-18 12:33:50 -0400431}