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