blob: cf9f58cb95985e2fd9b5b307ab56997bcb75adb7 [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;
Fabian Kozynski712ae392018-09-12 09:11:16 -04009
Aurimas Liutikasfd52c142018-04-17 09:50:46 -070010import androidx.viewpager.widget.PagerAdapter;
11import androidx.viewpager.widget.ViewPager;
Jason Monkcaf37622015-08-18 12:33:50 -040012import android.util.AttributeSet;
13import android.util.Log;
14import android.view.LayoutInflater;
15import android.view.View;
16import android.view.ViewGroup;
Amin Shaikha07a17b2018-02-23 16:02:52 -050017import android.view.animation.Interpolator;
18import android.view.animation.OvershootInterpolator;
19import android.widget.Scroller;
Jason Monkdf36aed2016-07-25 11:21:56 -040020
Jason Monkcaf37622015-08-18 12:33:50 -040021import com.android.systemui.R;
22import com.android.systemui.qs.QSPanel.QSTileLayout;
23import com.android.systemui.qs.QSPanel.TileRecord;
24
25import java.util.ArrayList;
Amin Shaikha07a17b2018-02-23 16:02:52 -050026import java.util.Set;
Jason Monkcaf37622015-08-18 12:33:50 -040027
28public class PagedTileLayout extends ViewPager implements QSTileLayout {
29
30 private static final boolean DEBUG = false;
31
32 private static final String TAG = "PagedTileLayout";
Amin Shaikha07a17b2018-02-23 16:02:52 -050033 private static final int REVEAL_SCROLL_DURATION_MILLIS = 750;
34 private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
35 private static final long BOUNCE_ANIMATION_DURATION = 450L;
36 private static final int TILE_ANIMATION_STAGGER_DELAY = 85;
37 private static final Interpolator SCROLL_CUBIC = (t) -> {
38 t -= 1.0f;
39 return t * t * t + 1.0f;
40 };
41
Amin Shaikh9978c552018-03-22 07:24:41 -040042 private final ArrayList<TileRecord> mTiles = new ArrayList<>();
43 private final ArrayList<TilePage> mPages = new ArrayList<>();
Jason Monkcaf37622015-08-18 12:33:50 -040044
Jason Monkcaf37622015-08-18 12:33:50 -040045 private PageIndicator mPageIndicator;
Rohan Shah3090e792018-04-12 00:01:00 -040046 private float mPageIndicatorPosition;
Jason Monkcaf37622015-08-18 12:33:50 -040047
Jason Monk162011e2016-02-19 08:11:55 -050048 private PageListener mPageListener;
Jason Monkcaf37622015-08-18 12:33:50 -040049
Jason Monke5107a32016-05-31 15:40:58 -040050 private boolean mListening;
Amin Shaikha07a17b2018-02-23 16:02:52 -050051 private Scroller mScroller;
52
53 private AnimatorSet mBounceAnimatorSet;
Amin Shaikh4c9048c2018-04-20 11:27:46 -040054 private float mLastExpansion;
Fabian Kozynski712ae392018-09-12 09:11:16 -040055 private boolean mDistributeTiles = false;
Jason Monke5107a32016-05-31 15:40:58 -040056
Jason Monkcaf37622015-08-18 12:33:50 -040057 public PagedTileLayout(Context context, AttributeSet attrs) {
58 super(context, attrs);
Amin Shaikha07a17b2018-02-23 16:02:52 -050059 mScroller = new Scroller(context, SCROLL_CUBIC);
Jason Monkcaf37622015-08-18 12:33:50 -040060 setAdapter(mAdapter);
Amin Shaikha07a17b2018-02-23 16:02:52 -050061 setOnPageChangeListener(mOnPageChangeListener);
62 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -040063 }
64
65 @Override
Jason Monk51fb85a2016-03-28 14:06:04 -040066 public void onRtlPropertiesChanged(int layoutDirection) {
67 super.onRtlPropertiesChanged(layoutDirection);
68 setAdapter(mAdapter);
69 setCurrentItem(0, false);
70 }
71
72 @Override
73 public void setCurrentItem(int item, boolean smoothScroll) {
74 if (isLayoutRtl()) {
75 item = mPages.size() - 1 - item;
76 }
77 super.setCurrentItem(item, smoothScroll);
78 }
79
80 @Override
Jason Monke5107a32016-05-31 15:40:58 -040081 public void setListening(boolean listening) {
82 if (mListening == listening) return;
83 mListening = listening;
Amin Shaikh9978c552018-03-22 07:24:41 -040084 updateListening();
85 }
86
87 private void updateListening() {
88 for (TilePage tilePage : mPages) {
89 tilePage.setListening(tilePage.getParent() == null ? false : mListening);
Jason Monke5107a32016-05-31 15:40:58 -040090 }
91 }
92
Amin Shaikha07a17b2018-02-23 16:02:52 -050093 @Override
Amin Shaikha07a17b2018-02-23 16:02:52 -050094 public void computeScroll() {
95 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
Amin Shaikh5484d0f2018-07-31 10:44:11 -040096 fakeDragBy(getScrollX() - mScroller.getCurrX());
Amin Shaikha07a17b2018-02-23 16:02:52 -050097 // Keep on drawing until the animation has finished.
98 postInvalidateOnAnimation();
99 return;
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400100 } else if (isFakeDragging()) {
101 endFakeDrag();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500102 mBounceAnimatorSet.start();
103 setOffscreenPageLimit(1);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500104 }
105 super.computeScroll();
106 }
107
Jason Monke5107a32016-05-31 15:40:58 -0400108 @Override
Jason Monkc5bdafb2016-02-25 16:24:21 -0500109 public boolean hasOverlappingRendering() {
110 return false;
111 }
112
113 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400114 protected void onFinishInflate() {
115 super.onFinishInflate();
Jason Monk32508852017-01-18 09:17:13 -0500116 mPages.add((TilePage) LayoutInflater.from(getContext())
Jason Monke4e69302016-01-20 11:27:15 -0500117 .inflate(R.layout.qs_paged_page, this, false));
Jason Monkcaf37622015-08-18 12:33:50 -0400118 }
119
Jason Monk32508852017-01-18 09:17:13 -0500120 public void setPageIndicator(PageIndicator indicator) {
121 mPageIndicator = indicator;
Fabian Kozynski712ae392018-09-12 09:11:16 -0400122 mPageIndicator.setNumPages(mPages.size());
Rohan Shah3090e792018-04-12 00:01:00 -0400123 mPageIndicator.setLocation(mPageIndicatorPosition);
Jason Monk32508852017-01-18 09:17:13 -0500124 }
125
Jason Monkcaf37622015-08-18 12:33:50 -0400126 @Override
127 public int getOffsetTop(TileRecord tile) {
Jason Monkae5bd032016-03-02 14:38:31 -0500128 final ViewGroup parent = (ViewGroup) tile.tileView.getParent();
129 if (parent == null) return 0;
130 return parent.getTop() + getTop();
Jason Monkcaf37622015-08-18 12:33:50 -0400131 }
132
133 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400134 public void addTile(TileRecord tile) {
135 mTiles.add(tile);
Fabian Kozynski712ae392018-09-12 09:11:16 -0400136 mDistributeTiles = true;
137 requestLayout();
Jason Monkcaf37622015-08-18 12:33:50 -0400138 }
139
140 @Override
141 public void removeTile(TileRecord tile) {
142 if (mTiles.remove(tile)) {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400143 mDistributeTiles = true;
144 requestLayout();
Jason Monkcaf37622015-08-18 12:33:50 -0400145 }
146 }
147
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400148 @Override
149 public void setExpansion(float expansion) {
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400150 mLastExpansion = expansion;
151 updateSelected();
152 }
153
154 private void updateSelected() {
155 // Start the marquee when fully expanded and stop when fully collapsed. Leave as is for
156 // other expansion ratios since there is no way way to pause the marquee.
157 if (mLastExpansion > 0f && mLastExpansion < 1f) {
158 return;
159 }
160 boolean selected = mLastExpansion == 1f;
Rohan Shah2dbcb572018-05-25 10:51:22 -0700161
162 // Disable accessibility temporarily while we update selected state purely for the
163 // marquee. This will ensure that accessibility doesn't announce the TYPE_VIEW_SELECTED
164 // event on any of the children.
165 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
Amin Shaikh6f570742018-06-01 17:18:19 -0400166 int currentItem = isLayoutRtl() ? mPages.size() - 1 - getCurrentItem() : getCurrentItem();
Amin Shaikh4c9048c2018-04-20 11:27:46 -0400167 for (int i = 0; i < mPages.size(); i++) {
Amin Shaikh6f570742018-06-01 17:18:19 -0400168 mPages.get(i).setSelected(i == currentItem ? selected : false);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400169 }
Rohan Shah2dbcb572018-05-25 10:51:22 -0700170 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Amin Shaikh0f8ea5432018-03-27 11:09:27 -0400171 }
172
Jason Monk162011e2016-02-19 08:11:55 -0500173 public void setPageListener(PageListener listener) {
174 mPageListener = listener;
175 }
176
Jason Monkcaf37622015-08-18 12:33:50 -0400177 private void distributeTiles() {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400178 emptyAndInflateOrRemovePages();
179
180 final int tileCount = mPages.get(0).maxTiles();
Jason Monkcaf37622015-08-18 12:33:50 -0400181 if (DEBUG) Log.d(TAG, "Distributing tiles");
Jason Monkcaf37622015-08-18 12:33:50 -0400182 int index = 0;
183 final int NT = mTiles.size();
184 for (int i = 0; i < NT; i++) {
185 TileRecord tile = mTiles.get(i);
Fabian Kozynski712ae392018-09-12 09:11:16 -0400186 if (mPages.get(index).mRecords.size() == tileCount) index++;
187 if (DEBUG) {
188 Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
189 + index);
Jason Monkcaf37622015-08-18 12:33:50 -0400190 }
Jason Monkcaf37622015-08-18 12:33:50 -0400191 mPages.get(index).addTile(tile);
192 }
Fabian Kozynski712ae392018-09-12 09:11:16 -0400193 }
194
195 private void emptyAndInflateOrRemovePages() {
196 final int nTiles = mTiles.size();
197 int numPages = nTiles / mPages.get(0).maxTiles();
198 // Add one more not full page if needed
199 numPages += (nTiles % mPages.get(0).maxTiles() == 0 ? 0 : 1);
200
201 final int NP = mPages.size();
202 for (int i = 0; i < NP; i++) {
203 mPages.get(i).removeAllViews();
Jason Monkcaf37622015-08-18 12:33:50 -0400204 }
Fabian Kozynski712ae392018-09-12 09:11:16 -0400205 if (NP == numPages) {
206 return;
207 }
208 while (mPages.size() < numPages) {
209 if (DEBUG) Log.d(TAG, "Adding page");
210 mPages.add((TilePage) LayoutInflater.from(getContext())
211 .inflate(R.layout.qs_paged_page, this, false));
212 }
213 while (mPages.size() > numPages) {
214 if (DEBUG) Log.d(TAG, "Removing page");
215 mPages.remove(mPages.size() - 1);
216 }
217 mPageIndicator.setNumPages(mPages.size());
218 setAdapter(mAdapter);
219 mAdapter.notifyDataSetChanged();
220 setCurrentItem(0, false);
Jason Monkcaf37622015-08-18 12:33:50 -0400221 }
222
223 @Override
Jason Monk9d02a432016-01-20 16:33:46 -0500224 public boolean updateResources() {
Rohan Shah3090e792018-04-12 00:01:00 -0400225 // Update bottom padding, useful for removing extra space once the panel page indicator is
226 // hidden.
227 setPadding(0, 0, 0,
228 getContext().getResources().getDimensionPixelSize(
229 R.dimen.qs_paged_tile_layout_padding_bottom));
Jason Monk9d02a432016-01-20 16:33:46 -0500230 boolean changed = false;
Jason Monkcaf37622015-08-18 12:33:50 -0400231 for (int i = 0; i < mPages.size(); i++) {
Jason Monk9d02a432016-01-20 16:33:46 -0500232 changed |= mPages.get(i).updateResources();
Jason Monkcaf37622015-08-18 12:33:50 -0400233 }
Jason Monk9d02a432016-01-20 16:33:46 -0500234 if (changed) {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400235 mDistributeTiles = true;
236 requestLayout();
Jason Monk9d02a432016-01-20 16:33:46 -0500237 }
238 return changed;
Jason Monkcaf37622015-08-18 12:33:50 -0400239 }
240
241 @Override
242 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400243
244 final int nTiles = mTiles.size();
245 if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
246
247 // Only change the pages if the number of rows or columns (from updateResources) has
248 // changed or the tiles have changed
249 if (mPages.get(0).updateMaxRows(heightMeasureSpec, nTiles) || mDistributeTiles) {
250 mDistributeTiles = false;
251 distributeTiles();
252 }
253
254 final int nRows = mPages.get(0).mRows;
255 for (int i = 0; i < mPages.size(); i++) {
256 TilePage t = mPages.get(i);
257 t.mRows = nRows;
258 }
259 }
260
Jason Monkcaf37622015-08-18 12:33:50 -0400261 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Fabian Kozynski712ae392018-09-12 09:11:16 -0400262
Jason Monkcaf37622015-08-18 12:33:50 -0400263 // 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 Monk8fb77872016-03-03 16:39:42 -0500276 public int getColumnCount() {
277 if (mPages.size() == 0) return 0;
278 return mPages.get(0).mColumns;
279 }
280
Fabian Kozynski802279f2018-09-07 13:44:54 -0400281 public int getNumVisibleTiles() {
282 if (mPages.size() == 0) return 0;
283 TilePage currentPage = mPages.get(getCurrentItem());
284 return currentPage.mRecords.size();
285 }
286
Amin Shaikha07a17b2018-02-23 16:02:52 -0500287 public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400288 if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0 || !beginFakeDrag()) {
Amin Shaikha07a17b2018-02-23 16:02:52 -0500289 // Do not start the reveal animation unless there are tiles to animate, multiple
290 // TilePages available and the user has not already started dragging.
291 return;
292 }
293
294 final int lastPageNumber = mPages.size() - 1;
295 final TilePage lastPage = mPages.get(lastPageNumber);
296 final ArrayList<Animator> bounceAnims = new ArrayList<>();
297 for (TileRecord tr : lastPage.mRecords) {
298 if (tileSpecs.contains(tr.tile.getTileSpec())) {
299 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
300 }
301 }
302
303 if (bounceAnims.isEmpty()) {
304 // All tileSpecs are on the first page. Nothing to do.
305 // TODO: potentially show a bounce animation for first page QS tiles
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400306 endFakeDrag();
Amin Shaikha07a17b2018-02-23 16:02:52 -0500307 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 });
Amin Shaikh5484d0f2018-07-31 10:44:11 -0400319 setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated.
320 int dx = getWidth() * lastPageNumber;
321 mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0,
322 REVEAL_SCROLL_DURATION_MILLIS);
Amin Shaikha07a17b2018-02-23 16:02:52 -0500323 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 {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400366
Jason Monkcaf37622015-08-18 12:33:50 -0400367 public TilePage(Context context, AttributeSet attrs) {
368 super(context, attrs);
Jason Monkbd6dbb02015-09-03 15:46:25 -0400369 }
370
Jason Monkbd6dbb02015-09-03 15:46:25 -0400371 public boolean isFull() {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400372 return mRecords.size() >= mColumns * mRows;
373 }
374
375 public int maxTiles() {
376 return mColumns * mRows;
Jason Monkcaf37622015-08-18 12:33:50 -0400377 }
378 }
379
380 private final PagerAdapter mAdapter = new PagerAdapter() {
Amin Shaikh9978c552018-03-22 07:24:41 -0400381 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400382 public void destroyItem(ViewGroup container, int position, Object object) {
383 if (DEBUG) Log.d(TAG, "Destantiating " + position);
Jason Monkcaf37622015-08-18 12:33:50 -0400384 container.removeView((View) object);
Amin Shaikh9978c552018-03-22 07:24:41 -0400385 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400386 }
387
Amin Shaikh9978c552018-03-22 07:24:41 -0400388 @Override
Jason Monkcaf37622015-08-18 12:33:50 -0400389 public Object instantiateItem(ViewGroup container, int position) {
390 if (DEBUG) Log.d(TAG, "Instantiating " + position);
Jason Monk51fb85a2016-03-28 14:06:04 -0400391 if (isLayoutRtl()) {
392 position = mPages.size() - 1 - position;
393 }
Jason Monke4e69302016-01-20 11:27:15 -0500394 ViewGroup view = mPages.get(position);
Jason Monkcaf37622015-08-18 12:33:50 -0400395 container.addView(view);
Amin Shaikh9978c552018-03-22 07:24:41 -0400396 updateListening();
Jason Monkcaf37622015-08-18 12:33:50 -0400397 return view;
398 }
399
400 @Override
401 public int getCount() {
Fabian Kozynski712ae392018-09-12 09:11:16 -0400402 return mPages.size();
Jason Monkcaf37622015-08-18 12:33:50 -0400403 }
404
405 @Override
406 public boolean isViewFromObject(View view, Object object) {
407 return view == object;
408 }
409 };
Jason Monk162011e2016-02-19 08:11:55 -0500410
411 public interface PageListener {
Jason Monk66eaf312016-02-25 12:29:29 -0500412 void onPageChanged(boolean isFirst);
Jason Monk162011e2016-02-19 08:11:55 -0500413 }
Jason Monkcaf37622015-08-18 12:33:50 -0400414}