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 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 86 public void onRtlChanged() { 87 updateAnimators(); 88 } 89 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 98 public void setHost(QSTileHost qsh) { 99 mHost = qsh; 100 qsh.addCallback(this); 101 updateAnimators(); 102 } 103 104 @Override 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 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 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 135 public void onPageChanged(boolean isFirst) { 136 if (mOnFirstPage == isFirst) return; 137 if (!isFirst) { 138 clearAnimationState(); 139 } 140 mOnFirstPage = isFirst; 141 } 142 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 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 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 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 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 289 public void onAnimationAtStart() { 290 mQuickQsPanel.setVisibility(View.VISIBLE); 291 } 292 293 @Override 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 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 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 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 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