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