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 = 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 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 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 isIconInAnimatedRow(int count)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 getRelativePosition(int[] loc1, View view, View parent)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 getRelativePositionInt(int[] loc1, View view, View parent)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 setPosition(float position)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 onAnimationAtStart()332 public void onAnimationAtStart() { 333 mQuickQsPanel.setVisibility(View.VISIBLE); 334 } 335 336 @Override onAnimationAtEnd()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 onAnimationStarted()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 clearAnimationState()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 onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)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 onTilesChanged()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