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 QSAnimator(QS qs, QuickQSPanel quickPanel, QSPanel panel)77 public QSAnimator(QS qs, QuickQSPanel quickPanel, QSPanel panel) { 78 mQs = qs; 79 mQuickQsPanel = quickPanel; 80 mQsPanel = panel; 81 mQsPanel.addOnAttachStateChangeListener(this); 82 qs.getView().addOnLayoutChangeListener(this); 83 if (mQsPanel.isAttachedToWindow()) { 84 onViewAttachedToWindow(null); 85 } 86 QSTileLayout tileLayout = mQsPanel.getTileLayout(); 87 if (tileLayout instanceof PagedTileLayout) { 88 mPagedLayout = ((PagedTileLayout) tileLayout); 89 } else { 90 Log.w(TAG, "QS Not using page layout"); 91 } 92 panel.setPageListener(this); 93 } 94 onRtlChanged()95 public void onRtlChanged() { 96 updateAnimators(); 97 } 98 setOnKeyguard(boolean onKeyguard)99 public void setOnKeyguard(boolean onKeyguard) { 100 mOnKeyguard = onKeyguard; 101 mQuickQsPanel.setVisibility(mOnKeyguard ? View.INVISIBLE : View.VISIBLE); 102 if (mOnKeyguard) { 103 clearAnimationState(); 104 } 105 } 106 setHost(QSTileHost qsh)107 public void setHost(QSTileHost qsh) { 108 mHost = qsh; 109 qsh.addCallback(this); 110 updateAnimators(); 111 } 112 113 @Override onViewAttachedToWindow(View v)114 public void onViewAttachedToWindow(View v) { 115 Dependency.get(TunerService.class).addTunable(this, ALLOW_FANCY_ANIMATION, 116 MOVE_FULL_ROWS, QuickQSPanel.NUM_QUICK_TILES); 117 } 118 119 @Override onViewDetachedFromWindow(View v)120 public void onViewDetachedFromWindow(View v) { 121 if (mHost != null) { 122 mHost.removeCallback(this); 123 } 124 Dependency.get(TunerService.class).removeTunable(this); 125 } 126 127 @Override onTuningChanged(String key, String newValue)128 public void onTuningChanged(String key, String newValue) { 129 if (ALLOW_FANCY_ANIMATION.equals(key)) { 130 mAllowFancy = TunerService.parseIntegerSwitch(newValue, true); 131 if (!mAllowFancy) { 132 clearAnimationState(); 133 } 134 } else if (MOVE_FULL_ROWS.equals(key)) { 135 mFullRows = TunerService.parseIntegerSwitch(newValue, true); 136 } else if (QuickQSPanel.NUM_QUICK_TILES.equals(key)) { 137 mNumQuickTiles = mQuickQsPanel.getNumQuickTiles(mQs.getContext()); 138 clearAnimationState(); 139 } 140 updateAnimators(); 141 } 142 143 @Override onPageChanged(boolean isFirst)144 public void onPageChanged(boolean isFirst) { 145 if (mOnFirstPage == isFirst) return; 146 if (!isFirst) { 147 clearAnimationState(); 148 } 149 mOnFirstPage = isFirst; 150 } 151 updateAnimators()152 private void updateAnimators() { 153 TouchAnimator.Builder firstPageBuilder = new Builder(); 154 TouchAnimator.Builder translationXBuilder = new Builder(); 155 TouchAnimator.Builder translationYBuilder = new Builder(); 156 157 if (mQsPanel.getHost() == null) return; 158 Collection<QSTile> tiles = mQsPanel.getHost().getTiles(); 159 int count = 0; 160 int[] loc1 = new int[2]; 161 int[] loc2 = new int[2]; 162 int lastXDiff = 0; 163 int lastX = 0; 164 165 clearAnimationState(); 166 mAllViews.clear(); 167 mQuickQsViews.clear(); 168 169 QSTileLayout tileLayout = mQsPanel.getTileLayout(); 170 mAllViews.add((View) tileLayout); 171 int height = mQs.getView() != null ? mQs.getView().getMeasuredHeight() : 0; 172 int width = mQs.getView() != null ? mQs.getView().getMeasuredWidth() : 0; 173 int heightDiff = height - mQs.getHeader().getBottom() 174 + mQs.getHeader().getPaddingBottom(); 175 firstPageBuilder.addFloat(tileLayout, "translationY", heightDiff, 0); 176 177 for (QSTile tile : tiles) { 178 QSTileView tileView = mQsPanel.getTileView(tile); 179 if (tileView == null) { 180 Log.e(TAG, "tileView is null " + tile.getTileSpec()); 181 continue; 182 } 183 final View tileIcon = tileView.getIcon().getIconView(); 184 View view = mQs.getView(); 185 186 // This case: less tiles to animate in small displays. 187 if (count < mQuickQsPanel.getTileLayout().getNumVisibleTiles() && mAllowFancy) { 188 // Quick tiles. 189 QSTileView quickTileView = mQuickQsPanel.getTileView(tile); 190 if (quickTileView == null) continue; 191 192 lastX = loc1[0]; 193 getRelativePosition(loc1, quickTileView.getIcon().getIconView(), view); 194 getRelativePosition(loc2, tileIcon, view); 195 final int xDiff = loc2[0] - loc1[0]; 196 final int yDiff = loc2[1] - loc1[1]; 197 lastXDiff = loc1[0] - lastX; 198 199 if (count < tileLayout.getNumVisibleTiles()) { 200 // Move the quick tile right from its location to the new one. 201 translationXBuilder.addFloat(quickTileView, "translationX", 0, xDiff); 202 translationYBuilder.addFloat(quickTileView, "translationY", 0, yDiff); 203 204 // Counteract the parent translation on the tile. So we have a static base to 205 // animate the label position off from. 206 //firstPageBuilder.addFloat(tileView, "translationY", mQsPanel.getHeight(), 0); 207 208 // Move the real tile from the quick tile position to its final 209 // location. 210 translationXBuilder.addFloat(tileView, "translationX", -xDiff, 0); 211 translationYBuilder.addFloat(tileView, "translationY", -yDiff, 0); 212 213 } else { // These tiles disappear when expanding 214 firstPageBuilder.addFloat(quickTileView, "alpha", 1, 0); 215 translationYBuilder.addFloat(quickTileView, "translationY", 0, yDiff); 216 217 // xDiff is negative here and this makes it "more" negative 218 final int translationX = mQsPanel.isLayoutRtl() ? xDiff - width : xDiff + width; 219 translationXBuilder.addFloat(quickTileView, "translationX", 0, 220 translationX); 221 } 222 223 mQuickQsViews.add(tileView.getIconWithBackground()); 224 mAllViews.add(tileView.getIcon()); 225 mAllViews.add(quickTileView); 226 } else if (mFullRows && isIconInAnimatedRow(count)) { 227 // TODO: Refactor some of this, it shares a lot with the above block. 228 // Move the last tile position over by the last difference between quick tiles. 229 // This makes the extra icons seems as if they are coming from positions in the 230 // quick panel. 231 loc1[0] += lastXDiff; 232 getRelativePosition(loc2, tileIcon, view); 233 final int xDiff = loc2[0] - loc1[0]; 234 final int yDiff = loc2[1] - loc1[1]; 235 236 firstPageBuilder.addFloat(tileView, "translationY", heightDiff, 0); 237 translationXBuilder.addFloat(tileView, "translationX", -xDiff, 0); 238 translationYBuilder.addFloat(tileView, "translationY", -yDiff, 0); 239 translationYBuilder.addFloat(tileIcon, "translationY", -yDiff, 0); 240 241 mAllViews.add(tileIcon); 242 } else { 243 firstPageBuilder.addFloat(tileView, "alpha", 0, 1); 244 firstPageBuilder.addFloat(tileView, "translationY", -heightDiff, 0); 245 } 246 mAllViews.add(tileView); 247 count++; 248 } 249 if (mAllowFancy) { 250 // Make brightness appear static position and alpha in through second half. 251 View brightness = mQsPanel.getBrightnessView(); 252 if (brightness != null) { 253 firstPageBuilder.addFloat(brightness, "translationY", heightDiff, 0); 254 mBrightnessAnimator = new TouchAnimator.Builder() 255 .addFloat(brightness, "alpha", 0, 1) 256 .setStartDelay(.5f) 257 .build(); 258 mAllViews.add(brightness); 259 } else { 260 mBrightnessAnimator = null; 261 } 262 mFirstPageAnimator = firstPageBuilder 263 .setListener(this) 264 .build(); 265 // Fade in the tiles/labels as we reach the final position. 266 mFirstPageDelayedAnimator = new TouchAnimator.Builder() 267 .setStartDelay(EXPANDED_TILE_DELAY) 268 .addFloat(tileLayout, "alpha", 0, 1) 269 .addFloat(mQsPanel.getDivider(), "alpha", 0, 1) 270 .addFloat(mQsPanel.getFooter().getView(), "alpha", 0, 1).build(); 271 mAllViews.add(mQsPanel.getDivider()); 272 mAllViews.add(mQsPanel.getFooter().getView()); 273 float px = 0; 274 float py = 1; 275 if (tiles.size() <= 3) { 276 px = 1; 277 } else if (tiles.size() <= 6) { 278 px = .4f; 279 } 280 PathInterpolatorBuilder interpolatorBuilder = new PathInterpolatorBuilder(0, 0, px, py); 281 translationXBuilder.setInterpolator(interpolatorBuilder.getXInterpolator()); 282 translationYBuilder.setInterpolator(interpolatorBuilder.getYInterpolator()); 283 mTranslationXAnimator = translationXBuilder.build(); 284 mTranslationYAnimator = translationYBuilder.build(); 285 } 286 mNonfirstPageAnimator = new TouchAnimator.Builder() 287 .addFloat(mQuickQsPanel, "alpha", 1, 0) 288 .addFloat(mQsPanel.getDivider(), "alpha", 0, 1) 289 .setListener(mNonFirstPageListener) 290 .setEndDelay(.5f) 291 .build(); 292 mNonfirstPageDelayedAnimator = new TouchAnimator.Builder() 293 .setStartDelay(.14f) 294 .addFloat(tileLayout, "alpha", 0, 1).build(); 295 } 296 isIconInAnimatedRow(int count)297 private boolean isIconInAnimatedRow(int count) { 298 if (mPagedLayout == null) { 299 return false; 300 } 301 final int columnCount = mPagedLayout.getColumnCount(); 302 return count < ((mNumQuickTiles + columnCount - 1) / columnCount) * columnCount; 303 } 304 getRelativePosition(int[] loc1, View view, View parent)305 private void getRelativePosition(int[] loc1, View view, View parent) { 306 loc1[0] = 0 + view.getWidth() / 2; 307 loc1[1] = 0; 308 getRelativePositionInt(loc1, view, parent); 309 } 310 getRelativePositionInt(int[] loc1, View view, View parent)311 private void getRelativePositionInt(int[] loc1, View view, View parent) { 312 if(view == parent || view == null) return; 313 // Ignore tile pages as they can have some offset we don't want to take into account in 314 // RTL. 315 if (!(view instanceof PagedTileLayout.TilePage)) { 316 loc1[0] += view.getLeft(); 317 loc1[1] += view.getTop(); 318 } 319 getRelativePositionInt(loc1, (View) view.getParent(), parent); 320 } 321 setPosition(float position)322 public void setPosition(float position) { 323 if (mFirstPageAnimator == null) return; 324 if (mOnKeyguard) { 325 return; 326 } 327 mLastPosition = position; 328 if (mOnFirstPage && mAllowFancy) { 329 mQuickQsPanel.setAlpha(1); 330 mFirstPageAnimator.setPosition(position); 331 mFirstPageDelayedAnimator.setPosition(position); 332 mTranslationXAnimator.setPosition(position); 333 mTranslationYAnimator.setPosition(position); 334 if (mBrightnessAnimator != null) { 335 mBrightnessAnimator.setPosition(position); 336 } 337 } else { 338 mNonfirstPageAnimator.setPosition(position); 339 mNonfirstPageDelayedAnimator.setPosition(position); 340 } 341 } 342 343 @Override onAnimationAtStart()344 public void onAnimationAtStart() { 345 mQuickQsPanel.setVisibility(View.VISIBLE); 346 } 347 348 @Override onAnimationAtEnd()349 public void onAnimationAtEnd() { 350 mQuickQsPanel.setVisibility(View.INVISIBLE); 351 final int N = mQuickQsViews.size(); 352 for (int i = 0; i < N; i++) { 353 mQuickQsViews.get(i).setVisibility(View.VISIBLE); 354 } 355 } 356 357 @Override onAnimationStarted()358 public void onAnimationStarted() { 359 mQuickQsPanel.setVisibility(mOnKeyguard ? View.INVISIBLE : View.VISIBLE); 360 if (mOnFirstPage) { 361 final int N = mQuickQsViews.size(); 362 for (int i = 0; i < N; i++) { 363 mQuickQsViews.get(i).setVisibility(View.INVISIBLE); 364 } 365 } 366 } 367 clearAnimationState()368 private void clearAnimationState() { 369 final int N = mAllViews.size(); 370 mQuickQsPanel.setAlpha(0); 371 for (int i = 0; i < N; i++) { 372 View v = mAllViews.get(i); 373 v.setAlpha(1); 374 v.setTranslationX(0); 375 v.setTranslationY(0); 376 } 377 final int N2 = mQuickQsViews.size(); 378 for (int i = 0; i < N2; i++) { 379 mQuickQsViews.get(i).setVisibility(View.VISIBLE); 380 } 381 } 382 383 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)384 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 385 int oldTop, int oldRight, int oldBottom) { 386 mQsPanel.post(mUpdateAnimators); 387 } 388 389 @Override onTilesChanged()390 public void onTilesChanged() { 391 // Give the QS panels a moment to generate their new tiles, then create all new animators 392 // hooked up to the new views. 393 mQsPanel.post(mUpdateAnimators); 394 } 395 396 private final TouchAnimator.Listener mNonFirstPageListener = 397 new TouchAnimator.ListenerAdapter() { 398 @Override 399 public void onAnimationAtEnd() { 400 mQuickQsPanel.setVisibility(View.INVISIBLE); 401 } 402 403 @Override 404 public void onAnimationStarted() { 405 mQuickQsPanel.setVisibility(View.VISIBLE); 406 } 407 }; 408 409 private Runnable mUpdateAnimators = new Runnable() { 410 @Override 411 public void run() { 412 updateAnimators(); 413 setPosition(mLastPosition); 414 } 415 }; 416 } 417