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