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 = .7f; 45 private static final float LAST_ROW_EXPANDED_DELAY = .86f; 46 47 private final ArrayList<View> mAllViews = new ArrayList<>(); 48 private final ArrayList<View> mTopFiveQs = new ArrayList<>(); 49 private final QuickQSPanel mQuickQsPanel; 50 private final QSPanel mQsPanel; 51 private final QSContainer mQsContainer; 52 53 private PagedTileLayout mPagedLayout; 54 55 private boolean mOnFirstPage = true; 56 private TouchAnimator mFirstPageAnimator; 57 private TouchAnimator mFirstPageDelayedAnimator; 58 private TouchAnimator mTranslationXAnimator; 59 private TouchAnimator mTranslationYAnimator; 60 private TouchAnimator mNonfirstPageAnimator; 61 private TouchAnimator mLastRowAnimator; 62 63 private boolean mOnKeyguard; 64 65 private boolean mAllowFancy; 66 private boolean mFullRows; 67 private int mNumQuickTiles; 68 private float mLastPosition; 69 private QSTileHost mHost; 70 QSAnimator(QSContainer container, QuickQSPanel quickPanel, QSPanel panel)71 public QSAnimator(QSContainer container, QuickQSPanel quickPanel, QSPanel panel) { 72 mQsContainer = container; 73 mQuickQsPanel = quickPanel; 74 mQsPanel = panel; 75 mQsPanel.addOnAttachStateChangeListener(this); 76 container.addOnLayoutChangeListener(this); 77 QSTileLayout tileLayout = mQsPanel.getTileLayout(); 78 if (tileLayout instanceof PagedTileLayout) { 79 mPagedLayout = ((PagedTileLayout) tileLayout); 80 mPagedLayout.setPageListener(this); 81 } else { 82 Log.w(TAG, "QS Not using page layout"); 83 } 84 } 85 onRtlChanged()86 public void onRtlChanged() { 87 updateAnimators(); 88 } 89 setOnKeyguard(boolean onKeyguard)90 public void setOnKeyguard(boolean onKeyguard) { 91 mOnKeyguard = onKeyguard; 92 mQuickQsPanel.setVisibility(mOnKeyguard ? View.INVISIBLE : View.VISIBLE); 93 if (mOnKeyguard) { 94 clearAnimationState(); 95 } 96 } 97 setHost(QSTileHost qsh)98 public void setHost(QSTileHost qsh) { 99 mHost = qsh; 100 qsh.addCallback(this); 101 updateAnimators(); 102 } 103 104 @Override onViewAttachedToWindow(View v)105 public void onViewAttachedToWindow(View v) { 106 TunerService.get(mQsContainer.getContext()).addTunable(this, ALLOW_FANCY_ANIMATION, 107 MOVE_FULL_ROWS, QuickQSPanel.NUM_QUICK_TILES); 108 } 109 110 @Override onViewDetachedFromWindow(View v)111 public void onViewDetachedFromWindow(View v) { 112 if (mHost != null) { 113 mHost.removeCallback(this); 114 } 115 TunerService.get(mQsContainer.getContext()).removeTunable(this); 116 } 117 118 @Override onTuningChanged(String key, String newValue)119 public void onTuningChanged(String key, String newValue) { 120 if (ALLOW_FANCY_ANIMATION.equals(key)) { 121 mAllowFancy = newValue == null || Integer.parseInt(newValue) != 0; 122 if (!mAllowFancy) { 123 clearAnimationState(); 124 } 125 } else if (MOVE_FULL_ROWS.equals(key)) { 126 mFullRows = newValue == null || Integer.parseInt(newValue) != 0; 127 } else if (QuickQSPanel.NUM_QUICK_TILES.equals(key)) { 128 mNumQuickTiles = mQuickQsPanel.getNumQuickTiles(mQsContainer.getContext()); 129 clearAnimationState(); 130 } 131 updateAnimators(); 132 } 133 134 @Override onPageChanged(boolean isFirst)135 public void onPageChanged(boolean isFirst) { 136 if (mOnFirstPage == isFirst) return; 137 if (!isFirst) { 138 clearAnimationState(); 139 } 140 mOnFirstPage = isFirst; 141 } 142 updateAnimators()143 private void updateAnimators() { 144 TouchAnimator.Builder firstPageBuilder = new Builder(); 145 TouchAnimator.Builder translationXBuilder = new Builder(); 146 TouchAnimator.Builder translationYBuilder = new Builder(); 147 TouchAnimator.Builder lastRowBuilder = new Builder(); 148 149 if (mQsPanel.getHost() == null) return; 150 Collection<QSTile<?>> tiles = mQsPanel.getHost().getTiles(); 151 int count = 0; 152 int[] loc1 = new int[2]; 153 int[] loc2 = new int[2]; 154 int lastXDiff = 0; 155 int lastYDiff = 0; 156 int lastX = 0; 157 158 clearAnimationState(); 159 mAllViews.clear(); 160 mTopFiveQs.clear(); 161 162 mAllViews.add((View) mQsPanel.getTileLayout()); 163 164 for (QSTile<?> tile : tiles) { 165 QSTileBaseView tileView = mQsPanel.getTileView(tile); 166 final TextView label = ((QSTileView) tileView).getLabel(); 167 final View tileIcon = tileView.getIcon().getIconView(); 168 if (count < mNumQuickTiles && mAllowFancy) { 169 // Quick tiles. 170 QSTileBaseView quickTileView = mQuickQsPanel.getTileView(tile); 171 172 lastX = loc1[0]; 173 getRelativePosition(loc1, quickTileView.getIcon(), mQsContainer); 174 getRelativePosition(loc2, tileIcon, mQsContainer); 175 final int xDiff = loc2[0] - loc1[0]; 176 final int yDiff = loc2[1] - loc1[1]; 177 lastXDiff = loc1[0] - lastX; 178 lastYDiff = yDiff; 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 lastRowBuilder.addFloat(tileView, "alpha", 0, 1); 213 } 214 mAllViews.add(tileView); 215 mAllViews.add(label); 216 count++; 217 } 218 if (mAllowFancy) { 219 mFirstPageAnimator = firstPageBuilder 220 .setListener(this) 221 .build(); 222 // Fade in the tiles/labels as we reach the final position. 223 mFirstPageDelayedAnimator = new TouchAnimator.Builder() 224 .setStartDelay(EXPANDED_TILE_DELAY) 225 .addFloat(mQsPanel.getTileLayout(), "alpha", 0, 1).build(); 226 mLastRowAnimator = lastRowBuilder 227 .setStartDelay(LAST_ROW_EXPANDED_DELAY) 228 .build(); 229 Path path = new Path(); 230 path.moveTo(0, 0); 231 path.cubicTo(0, 0, 0, 1, 1, 1); 232 PathInterpolatorBuilder interpolatorBuilder = new PathInterpolatorBuilder(0, 0, 0, 1); 233 translationXBuilder.setInterpolator(interpolatorBuilder.getXInterpolator()); 234 translationYBuilder.setInterpolator(interpolatorBuilder.getYInterpolator()); 235 mTranslationXAnimator = translationXBuilder.build(); 236 mTranslationYAnimator = translationYBuilder.build(); 237 } 238 mNonfirstPageAnimator = new TouchAnimator.Builder() 239 .addFloat(mQuickQsPanel, "alpha", 1, 0) 240 .setListener(mNonFirstPageListener) 241 .setEndDelay(.5f) 242 .build(); 243 } 244 isIconInAnimatedRow(int count)245 private boolean isIconInAnimatedRow(int count) { 246 if (mPagedLayout == null) { 247 return false; 248 } 249 final int columnCount = mPagedLayout.getColumnCount(); 250 return count < ((mNumQuickTiles + columnCount - 1) / columnCount) * columnCount; 251 } 252 getRelativePosition(int[] loc1, View view, View parent)253 private void getRelativePosition(int[] loc1, View view, View parent) { 254 loc1[0] = 0 + view.getWidth() / 2; 255 loc1[1] = 0; 256 getRelativePositionInt(loc1, view, parent); 257 } 258 getRelativePositionInt(int[] loc1, View view, View parent)259 private void getRelativePositionInt(int[] loc1, View view, View parent) { 260 if(view == parent || view == null) return; 261 // Ignore tile pages as they can have some offset we don't want to take into account in 262 // RTL. 263 if (!(view instanceof PagedTileLayout.TilePage)) { 264 loc1[0] += view.getLeft(); 265 loc1[1] += view.getTop(); 266 } 267 getRelativePositionInt(loc1, (View) view.getParent(), parent); 268 } 269 setPosition(float position)270 public void setPosition(float position) { 271 if (mFirstPageAnimator == null) return; 272 if (mOnKeyguard) { 273 return; 274 } 275 mLastPosition = position; 276 if (mOnFirstPage && mAllowFancy) { 277 mQuickQsPanel.setAlpha(1); 278 mFirstPageAnimator.setPosition(position); 279 mFirstPageDelayedAnimator.setPosition(position); 280 mTranslationXAnimator.setPosition(position); 281 mTranslationYAnimator.setPosition(position); 282 mLastRowAnimator.setPosition(position); 283 } else { 284 mNonfirstPageAnimator.setPosition(position); 285 } 286 } 287 288 @Override onAnimationAtStart()289 public void onAnimationAtStart() { 290 mQuickQsPanel.setVisibility(View.VISIBLE); 291 } 292 293 @Override onAnimationAtEnd()294 public void onAnimationAtEnd() { 295 mQuickQsPanel.setVisibility(View.INVISIBLE); 296 final int N = mTopFiveQs.size(); 297 for (int i = 0; i < N; i++) { 298 mTopFiveQs.get(i).setVisibility(View.VISIBLE); 299 } 300 } 301 302 @Override onAnimationStarted()303 public void onAnimationStarted() { 304 mQuickQsPanel.setVisibility(mOnKeyguard ? View.INVISIBLE : View.VISIBLE); 305 if (mOnFirstPage) { 306 final int N = mTopFiveQs.size(); 307 for (int i = 0; i < N; i++) { 308 mTopFiveQs.get(i).setVisibility(View.INVISIBLE); 309 } 310 } 311 } 312 clearAnimationState()313 private void clearAnimationState() { 314 final int N = mAllViews.size(); 315 mQuickQsPanel.setAlpha(0); 316 for (int i = 0; i < N; i++) { 317 View v = mAllViews.get(i); 318 v.setAlpha(1); 319 v.setTranslationX(0); 320 v.setTranslationY(0); 321 } 322 final int N2 = mTopFiveQs.size(); 323 for (int i = 0; i < N2; i++) { 324 mTopFiveQs.get(i).setVisibility(View.VISIBLE); 325 } 326 } 327 328 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)329 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 330 int oldTop, int oldRight, int oldBottom) { 331 mQsPanel.post(mUpdateAnimators); 332 } 333 334 @Override onTilesChanged()335 public void onTilesChanged() { 336 // Give the QS panels a moment to generate their new tiles, then create all new animators 337 // hooked up to the new views. 338 mQsPanel.post(mUpdateAnimators); 339 } 340 341 private final TouchAnimator.Listener mNonFirstPageListener = 342 new TouchAnimator.ListenerAdapter() { 343 @Override 344 public void onAnimationStarted() { 345 mQuickQsPanel.setVisibility(View.VISIBLE); 346 } 347 }; 348 349 private Runnable mUpdateAnimators = new Runnable() { 350 @Override 351 public void run() { 352 updateAnimators(); 353 setPosition(mLastPosition); 354 } 355 }; 356 } 357