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