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 15package com.android.systemui.qs; 16 17import android.util.Log; 18import android.view.View; 19import android.view.View.OnAttachStateChangeListener; 20import android.view.View.OnLayoutChangeListener; 21 22import com.android.systemui.Dependency; 23import com.android.systemui.plugins.qs.*; 24import com.android.systemui.plugins.qs.QSTileView; 25import com.android.systemui.qs.PagedTileLayout.PageListener; 26import com.android.systemui.qs.QSPanel.QSTileLayout; 27import com.android.systemui.qs.QSHost.Callback; 28import com.android.systemui.qs.TouchAnimator.Builder; 29import com.android.systemui.qs.TouchAnimator.Listener; 30import com.android.systemui.tuner.TunerService; 31import com.android.systemui.tuner.TunerService.Tunable; 32 33import java.util.ArrayList; 34import java.util.Collection; 35 36public 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 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 88 public void onRtlChanged() { 89 updateAnimators(); 90 } 91 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 100 public void setHost(QSTileHost qsh) { 101 mHost = qsh; 102 qsh.addCallback(this); 103 updateAnimators(); 104 } 105 106 @Override 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 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 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 137 public void onPageChanged(boolean isFirst) { 138 if (mOnFirstPage == isFirst) return; 139 if (!isFirst) { 140 clearAnimationState(); 141 } 142 mOnFirstPage = isFirst; 143 } 144 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 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 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 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 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 321 public void onAnimationAtStart() { 322 mQuickQsPanel.setVisibility(View.VISIBLE); 323 } 324 325 @Override 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 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 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 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 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