1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.qs; 16 17 import android.util.Log; 18 import android.view.View; 19 import android.view.View.OnAttachStateChangeListener; 20 import android.view.View.OnLayoutChangeListener; 21 22 import com.android.systemui.Dependency; 23 import com.android.systemui.plugins.qs.QS; 24 import com.android.systemui.plugins.qs.QSTile; 25 import com.android.systemui.plugins.qs.QSTileView; 26 import com.android.systemui.qs.PagedTileLayout.PageListener; 27 import com.android.systemui.qs.QSHost.Callback; 28 import com.android.systemui.qs.QSPanel.QSTileLayout; 29 import com.android.systemui.qs.TouchAnimator.Builder; 30 import com.android.systemui.qs.TouchAnimator.Listener; 31 import com.android.systemui.tuner.TunerService; 32 import com.android.systemui.tuner.TunerService.Tunable; 33 34 import java.util.ArrayList; 35 import java.util.Collection; 36 37 public class QSAnimator implements Callback, PageListener, Listener, OnLayoutChangeListener, 38 OnAttachStateChangeListener, Tunable { 39 40 private static final String TAG = "QSAnimator"; 41 42 private static final String ALLOW_FANCY_ANIMATION = "sysui_qs_fancy_anim"; 43 private static final String MOVE_FULL_ROWS = "sysui_qs_move_whole_rows"; 44 45 public static final float EXPANDED_TILE_DELAY = .86f; 46 47 48 private final ArrayList<View> mAllViews = new ArrayList<>(); 49 /** 50 * List of {@link View}s representing Quick Settings that are being animated from the quick QS 51 * position to the normal QS panel. 52 */ 53 private final ArrayList<View> mQuickQsViews = new ArrayList<>(); 54 private final QuickQSPanel mQuickQsPanel; 55 private final QSPanel mQsPanel; 56 private final QS mQs; 57 58 private PagedTileLayout mPagedLayout; 59 60 private boolean mOnFirstPage = true; 61 private TouchAnimator mFirstPageAnimator; 62 private TouchAnimator mFirstPageDelayedAnimator; 63 private TouchAnimator mTranslationXAnimator; 64 private TouchAnimator mTranslationYAnimator; 65 private TouchAnimator mNonfirstPageAnimator; 66 private TouchAnimator mNonfirstPageDelayedAnimator; 67 private TouchAnimator mBrightnessAnimator; 68 69 private boolean mOnKeyguard; 70 71 private boolean mAllowFancy; 72 private boolean mFullRows; 73 private int mNumQuickTiles; 74 private float mLastPosition; 75 private QSTileHost mHost; 76 private boolean mShowCollapsedOnKeyguard; 77 QSAnimator(QS qs, QuickQSPanel quickPanel, QSPanel panel)78 public QSAnimator(QS qs, QuickQSPanel quickPanel, QSPanel panel) { 79 mQs = qs; 80 mQuickQsPanel = quickPanel; 81 mQsPanel = panel; 82 mQsPanel.addOnAttachStateChangeListener(this); 83 qs.getView().addOnLayoutChangeListener(this); 84 if (mQsPanel.isAttachedToWindow()) { 85 onViewAttachedToWindow(null); 86 } 87 QSTileLayout tileLayout = mQsPanel.getTileLayout(); 88 if (tileLayout instanceof PagedTileLayout) { 89 mPagedLayout = ((PagedTileLayout) tileLayout); 90 } else { 91 Log.w(TAG, "QS Not using page layout"); 92 } 93 panel.setPageListener(this); 94 } 95 onRtlChanged()96 public void onRtlChanged() { 97 updateAnimators(); 98 } 99 setOnKeyguard(boolean onKeyguard)100 public void setOnKeyguard(boolean onKeyguard) { 101 mOnKeyguard = onKeyguard; 102 updateQQSVisibility(); 103 if (mOnKeyguard) { 104 clearAnimationState(); 105 } 106 } 107 108 109 /** 110 * Sets whether or not the keyguard is currently being shown with a collapsed header. 111 */ setShowCollapsedOnKeyguard(boolean showCollapsedOnKeyguard)112 void setShowCollapsedOnKeyguard(boolean showCollapsedOnKeyguard) { 113 mShowCollapsedOnKeyguard = showCollapsedOnKeyguard; 114 updateQQSVisibility(); 115 setCurrentPosition(); 116 } 117 118 setCurrentPosition()119 private void setCurrentPosition() { 120 setPosition(mLastPosition); 121 } 122 updateQQSVisibility()123 private void updateQQSVisibility() { 124 mQuickQsPanel.setVisibility(mOnKeyguard 125 && !mShowCollapsedOnKeyguard ? View.INVISIBLE : View.VISIBLE); 126 } 127 setHost(QSTileHost qsh)128 public void setHost(QSTileHost qsh) { 129 mHost = qsh; 130 qsh.addCallback(this); 131 updateAnimators(); 132 } 133 134 @Override onViewAttachedToWindow(View v)135 public void onViewAttachedToWindow(View v) { 136 Dependency.get(TunerService.class).addTunable(this, ALLOW_FANCY_ANIMATION, 137 MOVE_FULL_ROWS, QuickQSPanel.NUM_QUICK_TILES); 138 } 139 140 @Override onViewDetachedFromWindow(View v)141 public void onViewDetachedFromWindow(View v) { 142 if (mHost != null) { 143 mHost.removeCallback(this); 144 } 145 Dependency.get(TunerService.class).removeTunable(this); 146 } 147 148 @Override onTuningChanged(String key, String newValue)149 public void onTuningChanged(String key, String newValue) { 150 if (ALLOW_FANCY_ANIMATION.equals(key)) { 151 mAllowFancy = TunerService.parseIntegerSwitch(newValue, true); 152 if (!mAllowFancy) { 153 clearAnimationState(); 154 } 155 } else if (MOVE_FULL_ROWS.equals(key)) { 156 mFullRows = TunerService.parseIntegerSwitch(newValue, true); 157 } else if (QuickQSPanel.NUM_QUICK_TILES.equals(key)) { 158 mNumQuickTiles = mQuickQsPanel.getNumQuickTiles(mQs.getContext()); 159 clearAnimationState(); 160 } 161 updateAnimators(); 162 } 163 164 @Override onPageChanged(boolean isFirst)165 public void onPageChanged(boolean isFirst) { 166 if (mOnFirstPage == isFirst) return; 167 if (!isFirst) { 168 clearAnimationState(); 169 } 170 mOnFirstPage = isFirst; 171 } 172 updateAnimators()173 private void updateAnimators() { 174 TouchAnimator.Builder firstPageBuilder = new Builder(); 175 TouchAnimator.Builder translationXBuilder = new Builder(); 176 TouchAnimator.Builder translationYBuilder = new Builder(); 177 178 if (mQsPanel.getHost() == null) return; 179 Collection<QSTile> tiles = mQsPanel.getHost().getTiles(); 180 int count = 0; 181 int[] loc1 = new int[2]; 182 int[] loc2 = new int[2]; 183 int lastXDiff = 0; 184 int lastX = 0; 185 186 clearAnimationState(); 187 mAllViews.clear(); 188 mQuickQsViews.clear(); 189 190 QSTileLayout tileLayout = mQsPanel.getTileLayout(); 191 mAllViews.add((View) tileLayout); 192 int height = mQs.getView() != null ? mQs.getView().getMeasuredHeight() : 0; 193 int width = mQs.getView() != null ? mQs.getView().getMeasuredWidth() : 0; 194 int heightDiff = height - mQs.getHeader().getBottom() 195 + mQs.getHeader().getPaddingBottom(); 196 firstPageBuilder.addFloat(tileLayout, "translationY", heightDiff, 0); 197 198 for (QSTile tile : tiles) { 199 QSTileView tileView = mQsPanel.getTileView(tile); 200 if (tileView == null) { 201 Log.e(TAG, "tileView is null " + tile.getTileSpec()); 202 continue; 203 } 204 final View tileIcon = tileView.getIcon().getIconView(); 205 View view = mQs.getView(); 206 207 // This case: less tiles to animate in small displays. 208 if (count < mQuickQsPanel.getTileLayout().getNumVisibleTiles() && mAllowFancy) { 209 // Quick tiles. 210 QSTileView quickTileView = mQuickQsPanel.getTileView(tile); 211 if (quickTileView == null) continue; 212 213 lastX = loc1[0]; 214 getRelativePosition(loc1, quickTileView.getIcon().getIconView(), view); 215 getRelativePosition(loc2, tileIcon, view); 216 final int xDiff = loc2[0] - loc1[0]; 217 final int yDiff = loc2[1] - loc1[1]; 218 lastXDiff = loc1[0] - lastX; 219 220 if (count < tileLayout.getNumVisibleTiles()) { 221 // Move the quick tile right from its location to the new one. 222 translationXBuilder.addFloat(quickTileView, "translationX", 0, xDiff); 223 translationYBuilder.addFloat(quickTileView, "translationY", 0, yDiff); 224 225 // Counteract the parent translation on the tile. So we have a static base to 226 // animate the label position off from. 227 //firstPageBuilder.addFloat(tileView, "translationY", mQsPanel.getHeight(), 0); 228 229 // Move the real tile from the quick tile position to its final 230 // location. 231 translationXBuilder.addFloat(tileView, "translationX", -xDiff, 0); 232 translationYBuilder.addFloat(tileView, "translationY", -yDiff, 0); 233 234 } else { // These tiles disappear when expanding 235 firstPageBuilder.addFloat(quickTileView, "alpha", 1, 0); 236 translationYBuilder.addFloat(quickTileView, "translationY", 0, yDiff); 237 238 // xDiff is negative here and this makes it "more" negative 239 final int translationX = mQsPanel.isLayoutRtl() ? xDiff - width : xDiff + width; 240 translationXBuilder.addFloat(quickTileView, "translationX", 0, 241 translationX); 242 } 243 244 mQuickQsViews.add(tileView.getIconWithBackground()); 245 mAllViews.add(tileView.getIcon()); 246 mAllViews.add(quickTileView); 247 } else if (mFullRows && isIconInAnimatedRow(count)) { 248 // TODO: Refactor some of this, it shares a lot with the above block. 249 // Move the last tile position over by the last difference between quick tiles. 250 // This makes the extra icons seems as if they are coming from positions in the 251 // quick panel. 252 loc1[0] += lastXDiff; 253 getRelativePosition(loc2, tileIcon, view); 254 final int xDiff = loc2[0] - loc1[0]; 255 final int yDiff = loc2[1] - loc1[1]; 256 257 firstPageBuilder.addFloat(tileView, "translationY", heightDiff, 0); 258 translationXBuilder.addFloat(tileView, "translationX", -xDiff, 0); 259 translationYBuilder.addFloat(tileView, "translationY", -yDiff, 0); 260 translationYBuilder.addFloat(tileIcon, "translationY", -yDiff, 0); 261 262 mAllViews.add(tileIcon); 263 } else { 264 firstPageBuilder.addFloat(tileView, "alpha", 0, 1); 265 firstPageBuilder.addFloat(tileView, "translationY", -heightDiff, 0); 266 } 267 mAllViews.add(tileView); 268 count++; 269 } 270 if (mAllowFancy) { 271 // Make brightness appear static position and alpha in through second half. 272 View brightness = mQsPanel.getBrightnessView(); 273 if (brightness != null) { 274 firstPageBuilder.addFloat(brightness, "translationY", heightDiff, 0); 275 mBrightnessAnimator = new TouchAnimator.Builder() 276 .addFloat(brightness, "alpha", 0, 1) 277 .setStartDelay(.5f) 278 .build(); 279 mAllViews.add(brightness); 280 } else { 281 mBrightnessAnimator = null; 282 } 283 mFirstPageAnimator = firstPageBuilder 284 .setListener(this) 285 .build(); 286 // Fade in the tiles/labels as we reach the final position. 287 mFirstPageDelayedAnimator = new TouchAnimator.Builder() 288 .setStartDelay(EXPANDED_TILE_DELAY) 289 .addFloat(tileLayout, "alpha", 0, 1) 290 .addFloat(mQsPanel.getDivider(), "alpha", 0, 1) 291 .addFloat(mQsPanel.getFooter().getView(), "alpha", 0, 1).build(); 292 mAllViews.add(mQsPanel.getDivider()); 293 mAllViews.add(mQsPanel.getFooter().getView()); 294 float px = 0; 295 float py = 1; 296 if (tiles.size() <= 3) { 297 px = 1; 298 } else if (tiles.size() <= 6) { 299 px = .4f; 300 } 301 PathInterpolatorBuilder interpolatorBuilder = new PathInterpolatorBuilder(0, 0, px, py); 302 translationXBuilder.setInterpolator(interpolatorBuilder.getXInterpolator()); 303 translationYBuilder.setInterpolator(interpolatorBuilder.getYInterpolator()); 304 mTranslationXAnimator = translationXBuilder.build(); 305 mTranslationYAnimator = translationYBuilder.build(); 306 } 307 mNonfirstPageAnimator = new TouchAnimator.Builder() 308 .addFloat(mQuickQsPanel, "alpha", 1, 0) 309 .addFloat(mQsPanel.getDivider(), "alpha", 0, 1) 310 .setListener(mNonFirstPageListener) 311 .setEndDelay(.5f) 312 .build(); 313 mNonfirstPageDelayedAnimator = new TouchAnimator.Builder() 314 .setStartDelay(.14f) 315 .addFloat(tileLayout, "alpha", 0, 1).build(); 316 } 317 isIconInAnimatedRow(int count)318 private boolean isIconInAnimatedRow(int count) { 319 if (mPagedLayout == null) { 320 return false; 321 } 322 final int columnCount = mPagedLayout.getColumnCount(); 323 return count < ((mNumQuickTiles + columnCount - 1) / columnCount) * columnCount; 324 } 325 getRelativePosition(int[] loc1, View view, View parent)326 private void getRelativePosition(int[] loc1, View view, View parent) { 327 loc1[0] = 0 + view.getWidth() / 2; 328 loc1[1] = 0; 329 getRelativePositionInt(loc1, view, parent); 330 } 331 getRelativePositionInt(int[] loc1, View view, View parent)332 private void getRelativePositionInt(int[] loc1, View view, View parent) { 333 if(view == parent || view == null) return; 334 // Ignore tile pages as they can have some offset we don't want to take into account in 335 // RTL. 336 if (!(view instanceof PagedTileLayout.TilePage)) { 337 loc1[0] += view.getLeft(); 338 loc1[1] += view.getTop(); 339 } 340 getRelativePositionInt(loc1, (View) view.getParent(), parent); 341 } 342 setPosition(float position)343 public void setPosition(float position) { 344 if (mFirstPageAnimator == null) return; 345 if (mOnKeyguard) { 346 if (mShowCollapsedOnKeyguard) { 347 position = 0; 348 } else { 349 position = 1; 350 } 351 } 352 mLastPosition = position; 353 if (mOnFirstPage && mAllowFancy) { 354 mQuickQsPanel.setAlpha(1); 355 mFirstPageAnimator.setPosition(position); 356 mFirstPageDelayedAnimator.setPosition(position); 357 mTranslationXAnimator.setPosition(position); 358 mTranslationYAnimator.setPosition(position); 359 if (mBrightnessAnimator != null) { 360 mBrightnessAnimator.setPosition(position); 361 } 362 } else { 363 mNonfirstPageAnimator.setPosition(position); 364 mNonfirstPageDelayedAnimator.setPosition(position); 365 } 366 } 367 368 @Override onAnimationAtStart()369 public void onAnimationAtStart() { 370 mQuickQsPanel.setVisibility(View.VISIBLE); 371 } 372 373 @Override onAnimationAtEnd()374 public void onAnimationAtEnd() { 375 mQuickQsPanel.setVisibility(View.INVISIBLE); 376 final int N = mQuickQsViews.size(); 377 for (int i = 0; i < N; i++) { 378 mQuickQsViews.get(i).setVisibility(View.VISIBLE); 379 } 380 } 381 382 @Override onAnimationStarted()383 public void onAnimationStarted() { 384 updateQQSVisibility(); 385 if (mOnFirstPage) { 386 final int N = mQuickQsViews.size(); 387 for (int i = 0; i < N; i++) { 388 mQuickQsViews.get(i).setVisibility(View.INVISIBLE); 389 } 390 } 391 } 392 clearAnimationState()393 private void clearAnimationState() { 394 final int N = mAllViews.size(); 395 mQuickQsPanel.setAlpha(0); 396 for (int i = 0; i < N; i++) { 397 View v = mAllViews.get(i); 398 v.setAlpha(1); 399 v.setTranslationX(0); 400 v.setTranslationY(0); 401 } 402 final int N2 = mQuickQsViews.size(); 403 for (int i = 0; i < N2; i++) { 404 mQuickQsViews.get(i).setVisibility(View.VISIBLE); 405 } 406 } 407 408 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)409 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 410 int oldTop, int oldRight, int oldBottom) { 411 mQsPanel.post(mUpdateAnimators); 412 } 413 414 @Override onTilesChanged()415 public void onTilesChanged() { 416 // Give the QS panels a moment to generate their new tiles, then create all new animators 417 // hooked up to the new views. 418 mQsPanel.post(mUpdateAnimators); 419 } 420 421 private final TouchAnimator.Listener mNonFirstPageListener = 422 new TouchAnimator.ListenerAdapter() { 423 @Override 424 public void onAnimationAtEnd() { 425 mQuickQsPanel.setVisibility(View.INVISIBLE); 426 } 427 428 @Override 429 public void onAnimationStarted() { 430 mQuickQsPanel.setVisibility(View.VISIBLE); 431 } 432 }; 433 434 private Runnable mUpdateAnimators = new Runnable() { 435 @Override 436 public void run() { 437 updateAnimators(); 438 setCurrentPosition(); 439 } 440 }; 441 } 442