• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1  /*
2   * Copyright (C) 2014 The Android Open Source Project
3   *
4   * Licensed under the Apache License, Version 2.0 (the "License");
5   * you may not use this file except in compliance with the License.
6   * You may obtain a copy of the License at
7   *
8   *      http://www.apache.org/licenses/LICENSE-2.0
9   *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License
15   */
16  
17  package com.android.systemui.statusbar;
18  
19  import android.animation.Animator;
20  import android.animation.AnimatorListenerAdapter;
21  import android.animation.ArgbEvaluator;
22  import android.animation.PropertyValuesHolder;
23  import android.animation.ValueAnimator;
24  import android.annotation.Nullable;
25  import android.content.Context;
26  import android.content.res.TypedArray;
27  import android.graphics.Canvas;
28  import android.graphics.CanvasProperty;
29  import android.graphics.Color;
30  import android.graphics.Paint;
31  import android.graphics.PorterDuff;
32  import android.graphics.RecordingCanvas;
33  import android.graphics.drawable.Drawable;
34  import android.util.AttributeSet;
35  import android.view.RenderNodeAnimator;
36  import android.view.View;
37  import android.view.ViewAnimationUtils;
38  import android.view.animation.Interpolator;
39  import android.widget.ImageView;
40  
41  import com.android.systemui.Interpolators;
42  import com.android.systemui.R;
43  
44  /**
45   * An ImageView which does not have overlapping renderings commands and therefore does not need a
46   * layer when alpha is changed.
47   */
48  public class KeyguardAffordanceView extends ImageView {
49  
50      private static final long CIRCLE_APPEAR_DURATION = 80;
51      private static final long CIRCLE_DISAPPEAR_MAX_DURATION = 200;
52      private static final long NORMAL_ANIMATION_DURATION = 200;
53      public static final float MAX_ICON_SCALE_AMOUNT = 1.5f;
54      public static final float MIN_ICON_SCALE_AMOUNT = 0.8f;
55  
56      protected final int mDarkIconColor;
57      protected final int mNormalColor;
58      private final int mMinBackgroundRadius;
59      private final Paint mCirclePaint;
60      private final ArgbEvaluator mColorInterpolator;
61      private final FlingAnimationUtils mFlingAnimationUtils;
62      private float mCircleRadius;
63      private int mCenterX;
64      private int mCenterY;
65      private ValueAnimator mCircleAnimator;
66      private ValueAnimator mAlphaAnimator;
67      private ValueAnimator mScaleAnimator;
68      private float mCircleStartValue;
69      private boolean mCircleWillBeHidden;
70      private int[] mTempPoint = new int[2];
71      private float mImageScale = 1f;
72      private int mCircleColor;
73      private boolean mIsLeft;
74      private View mPreviewView;
75      private float mCircleStartRadius;
76      private float mMaxCircleSize;
77      private Animator mPreviewClipper;
78      private float mRestingAlpha = 1f;
79      private boolean mSupportHardware;
80      private boolean mFinishing;
81      private boolean mLaunchingAffordance;
82      private boolean mShouldTint = true;
83  
84      private CanvasProperty<Float> mHwCircleRadius;
85      private CanvasProperty<Float> mHwCenterX;
86      private CanvasProperty<Float> mHwCenterY;
87      private CanvasProperty<Paint> mHwCirclePaint;
88  
89      private AnimatorListenerAdapter mClipEndListener = new AnimatorListenerAdapter() {
90          @Override
91          public void onAnimationEnd(Animator animation) {
92              mPreviewClipper = null;
93          }
94      };
95      private AnimatorListenerAdapter mCircleEndListener = new AnimatorListenerAdapter() {
96          @Override
97          public void onAnimationEnd(Animator animation) {
98              mCircleAnimator = null;
99          }
100      };
101      private AnimatorListenerAdapter mScaleEndListener = new AnimatorListenerAdapter() {
102          @Override
103          public void onAnimationEnd(Animator animation) {
104              mScaleAnimator = null;
105          }
106      };
107      private AnimatorListenerAdapter mAlphaEndListener = new AnimatorListenerAdapter() {
108          @Override
109          public void onAnimationEnd(Animator animation) {
110              mAlphaAnimator = null;
111          }
112      };
113  
KeyguardAffordanceView(Context context)114      public KeyguardAffordanceView(Context context) {
115          this(context, null);
116      }
117  
KeyguardAffordanceView(Context context, AttributeSet attrs)118      public KeyguardAffordanceView(Context context, AttributeSet attrs) {
119          this(context, attrs, 0);
120      }
121  
KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr)122      public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr) {
123          this(context, attrs, defStyleAttr, 0);
124      }
125  
KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)126      public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr,
127              int defStyleRes) {
128          super(context, attrs, defStyleAttr, defStyleRes);
129          TypedArray a = context.obtainStyledAttributes(attrs, android.R.styleable.ImageView);
130  
131          mCirclePaint = new Paint();
132          mCirclePaint.setAntiAlias(true);
133          mCircleColor = 0xffffffff;
134          mCirclePaint.setColor(mCircleColor);
135  
136          mNormalColor = a.getColor(android.R.styleable.ImageView_tint, 0xffffffff);
137          mDarkIconColor = 0xff000000;
138          mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
139                  R.dimen.keyguard_affordance_min_background_radius);
140          mColorInterpolator = new ArgbEvaluator();
141          mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.3f);
142  
143          a.recycle();
144      }
145  
setImageDrawable(@ullable Drawable drawable, boolean tint)146      public void setImageDrawable(@Nullable Drawable drawable, boolean tint) {
147          super.setImageDrawable(drawable);
148          mShouldTint = tint;
149          updateIconColor();
150      }
151  
152      /**
153       * If current drawable should be tinted.
154       */
shouldTint()155      public boolean shouldTint() {
156          return mShouldTint;
157      }
158  
159      @Override
onLayout(boolean changed, int left, int top, int right, int bottom)160      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
161          super.onLayout(changed, left, top, right, bottom);
162          mCenterX = getWidth() / 2;
163          mCenterY = getHeight() / 2;
164          mMaxCircleSize = getMaxCircleSize();
165      }
166  
167      @Override
onDraw(Canvas canvas)168      protected void onDraw(Canvas canvas) {
169          mSupportHardware = canvas.isHardwareAccelerated();
170          drawBackgroundCircle(canvas);
171          canvas.save();
172          canvas.scale(mImageScale, mImageScale, getWidth() / 2, getHeight() / 2);
173          super.onDraw(canvas);
174          canvas.restore();
175      }
176  
setPreviewView(View v)177      public void setPreviewView(View v) {
178          if (mPreviewView == v) {
179              return;
180          }
181          View oldPreviewView = mPreviewView;
182          mPreviewView = v;
183          if (mPreviewView != null) {
184              mPreviewView.setVisibility(mLaunchingAffordance
185                      ? oldPreviewView.getVisibility() : INVISIBLE);
186          }
187      }
188  
updateIconColor()189      private void updateIconColor() {
190          if (!mShouldTint) return;
191          Drawable drawable = getDrawable().mutate();
192          float alpha = mCircleRadius / mMinBackgroundRadius;
193          alpha = Math.min(1.0f, alpha);
194          int color = (int) mColorInterpolator.evaluate(alpha, mNormalColor, mDarkIconColor);
195          drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
196      }
197  
drawBackgroundCircle(Canvas canvas)198      private void drawBackgroundCircle(Canvas canvas) {
199          if (mCircleRadius > 0 || mFinishing) {
200              if (mFinishing && mSupportHardware && mHwCenterX != null) {
201                  // Our hardware drawing proparties can be null if the finishing started but we have
202                  // never drawn before. In that case we are not doing a render thread animation
203                  // anyway, so we need to use the normal drawing.
204                  RecordingCanvas recordingCanvas = (RecordingCanvas) canvas;
205                  recordingCanvas.drawCircle(mHwCenterX, mHwCenterY, mHwCircleRadius,
206                          mHwCirclePaint);
207              } else {
208                  updateCircleColor();
209                  canvas.drawCircle(mCenterX, mCenterY, mCircleRadius, mCirclePaint);
210              }
211          }
212      }
213  
updateCircleColor()214      private void updateCircleColor() {
215          float fraction = 0.5f + 0.5f * Math.max(0.0f, Math.min(1.0f,
216                  (mCircleRadius - mMinBackgroundRadius) / (0.5f * mMinBackgroundRadius)));
217          if (mPreviewView != null && mPreviewView.getVisibility() == VISIBLE) {
218              float finishingFraction = 1 - Math.max(0, mCircleRadius - mCircleStartRadius)
219                      / (mMaxCircleSize - mCircleStartRadius);
220              fraction *= finishingFraction;
221          }
222          int color = Color.argb((int) (Color.alpha(mCircleColor) * fraction),
223                  Color.red(mCircleColor),
224                  Color.green(mCircleColor), Color.blue(mCircleColor));
225          mCirclePaint.setColor(color);
226      }
227  
finishAnimation(float velocity, final Runnable mAnimationEndRunnable)228      public void finishAnimation(float velocity, final Runnable mAnimationEndRunnable) {
229          cancelAnimator(mCircleAnimator);
230          cancelAnimator(mPreviewClipper);
231          mFinishing = true;
232          mCircleStartRadius = mCircleRadius;
233          final float maxCircleSize = getMaxCircleSize();
234          Animator animatorToRadius;
235          if (mSupportHardware) {
236              initHwProperties();
237              animatorToRadius = getRtAnimatorToRadius(maxCircleSize);
238              startRtAlphaFadeIn();
239          } else {
240              animatorToRadius = getAnimatorToRadius(maxCircleSize);
241          }
242          mFlingAnimationUtils.applyDismissing(animatorToRadius, mCircleRadius, maxCircleSize,
243                  velocity, maxCircleSize);
244          animatorToRadius.addListener(new AnimatorListenerAdapter() {
245              @Override
246              public void onAnimationEnd(Animator animation) {
247                  mAnimationEndRunnable.run();
248                  mFinishing = false;
249                  mCircleRadius = maxCircleSize;
250                  invalidate();
251              }
252          });
253          animatorToRadius.start();
254          setImageAlpha(0, true);
255          if (mPreviewView != null) {
256              mPreviewView.setVisibility(View.VISIBLE);
257              mPreviewClipper = ViewAnimationUtils.createCircularReveal(
258                      mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
259                      maxCircleSize);
260              mFlingAnimationUtils.applyDismissing(mPreviewClipper, mCircleRadius, maxCircleSize,
261                      velocity, maxCircleSize);
262              mPreviewClipper.addListener(mClipEndListener);
263              mPreviewClipper.start();
264              if (mSupportHardware) {
265                  startRtCircleFadeOut(animatorToRadius.getDuration());
266              }
267          }
268      }
269  
270      /**
271       * Fades in the Circle on the RenderThread. It's used when finishing the circle when it had
272       * alpha 0 in the beginning.
273       */
startRtAlphaFadeIn()274      private void startRtAlphaFadeIn() {
275          if (mCircleRadius == 0 && mPreviewView == null) {
276              Paint modifiedPaint = new Paint(mCirclePaint);
277              modifiedPaint.setColor(mCircleColor);
278              modifiedPaint.setAlpha(0);
279              mHwCirclePaint = CanvasProperty.createPaint(modifiedPaint);
280              RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
281                      RenderNodeAnimator.PAINT_ALPHA, 255);
282              animator.setTarget(this);
283              animator.setInterpolator(Interpolators.ALPHA_IN);
284              animator.setDuration(250);
285              animator.start();
286          }
287      }
288  
instantFinishAnimation()289      public void instantFinishAnimation() {
290          cancelAnimator(mPreviewClipper);
291          if (mPreviewView != null) {
292              mPreviewView.setClipBounds(null);
293              mPreviewView.setVisibility(View.VISIBLE);
294          }
295          mCircleRadius = getMaxCircleSize();
296          setImageAlpha(0, false);
297          invalidate();
298      }
299  
startRtCircleFadeOut(long duration)300      private void startRtCircleFadeOut(long duration) {
301          RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
302                  RenderNodeAnimator.PAINT_ALPHA, 0);
303          animator.setDuration(duration);
304          animator.setInterpolator(Interpolators.ALPHA_OUT);
305          animator.setTarget(this);
306          animator.start();
307      }
308  
getRtAnimatorToRadius(float circleRadius)309      private Animator getRtAnimatorToRadius(float circleRadius) {
310          RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius);
311          animator.setTarget(this);
312          return animator;
313      }
314  
initHwProperties()315      private void initHwProperties() {
316          mHwCenterX = CanvasProperty.createFloat(mCenterX);
317          mHwCenterY = CanvasProperty.createFloat(mCenterY);
318          mHwCirclePaint = CanvasProperty.createPaint(mCirclePaint);
319          mHwCircleRadius = CanvasProperty.createFloat(mCircleRadius);
320      }
321  
getMaxCircleSize()322      private float getMaxCircleSize() {
323          getLocationInWindow(mTempPoint);
324          float rootWidth = getRootView().getWidth();
325          float width = mTempPoint[0] + mCenterX;
326          width = Math.max(rootWidth - width, width);
327          float height = mTempPoint[1] + mCenterY;
328          return (float) Math.hypot(width, height);
329      }
330  
setCircleRadius(float circleRadius)331      public void setCircleRadius(float circleRadius) {
332          setCircleRadius(circleRadius, false, false);
333      }
334  
setCircleRadius(float circleRadius, boolean slowAnimation)335      public void setCircleRadius(float circleRadius, boolean slowAnimation) {
336          setCircleRadius(circleRadius, slowAnimation, false);
337      }
338  
setCircleRadiusWithoutAnimation(float circleRadius)339      public void setCircleRadiusWithoutAnimation(float circleRadius) {
340          cancelAnimator(mCircleAnimator);
341          setCircleRadius(circleRadius, false ,true);
342      }
343  
setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation)344      private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) {
345  
346          // Check if we need a new animation
347          boolean radiusHidden = (mCircleAnimator != null && mCircleWillBeHidden)
348                  || (mCircleAnimator == null && mCircleRadius == 0.0f);
349          boolean nowHidden = circleRadius == 0.0f;
350          boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation;
351          if (!radiusNeedsAnimation) {
352              if (mCircleAnimator == null) {
353                  mCircleRadius = circleRadius;
354                  updateIconColor();
355                  invalidate();
356                  if (nowHidden) {
357                      if (mPreviewView != null) {
358                          mPreviewView.setVisibility(View.INVISIBLE);
359                      }
360                  }
361              } else if (!mCircleWillBeHidden) {
362  
363                  // We just update the end value
364                  float diff = circleRadius - mMinBackgroundRadius;
365                  PropertyValuesHolder[] values = mCircleAnimator.getValues();
366                  values[0].setFloatValues(mCircleStartValue + diff, circleRadius);
367                  mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime());
368              }
369          } else {
370              cancelAnimator(mCircleAnimator);
371              cancelAnimator(mPreviewClipper);
372              ValueAnimator animator = getAnimatorToRadius(circleRadius);
373              Interpolator interpolator = circleRadius == 0.0f
374                      ? Interpolators.FAST_OUT_LINEAR_IN
375                      : Interpolators.LINEAR_OUT_SLOW_IN;
376              animator.setInterpolator(interpolator);
377              long duration = 250;
378              if (!slowAnimation) {
379                  float durationFactor = Math.abs(mCircleRadius - circleRadius)
380                          / (float) mMinBackgroundRadius;
381                  duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor);
382                  duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION);
383              }
384              animator.setDuration(duration);
385              animator.start();
386              if (mPreviewView != null && mPreviewView.getVisibility() == View.VISIBLE) {
387                  mPreviewView.setVisibility(View.VISIBLE);
388                  mPreviewClipper = ViewAnimationUtils.createCircularReveal(
389                          mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
390                          circleRadius);
391                  mPreviewClipper.setInterpolator(interpolator);
392                  mPreviewClipper.setDuration(duration);
393                  mPreviewClipper.addListener(mClipEndListener);
394                  mPreviewClipper.addListener(new AnimatorListenerAdapter() {
395                      @Override
396                      public void onAnimationEnd(Animator animation) {
397                          mPreviewView.setVisibility(View.INVISIBLE);
398                      }
399                  });
400                  mPreviewClipper.start();
401              }
402          }
403      }
404  
getAnimatorToRadius(float circleRadius)405      private ValueAnimator getAnimatorToRadius(float circleRadius) {
406          ValueAnimator animator = ValueAnimator.ofFloat(mCircleRadius, circleRadius);
407          mCircleAnimator = animator;
408          mCircleStartValue = mCircleRadius;
409          mCircleWillBeHidden = circleRadius == 0.0f;
410          animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
411              @Override
412              public void onAnimationUpdate(ValueAnimator animation) {
413                  mCircleRadius = (float) animation.getAnimatedValue();
414                  updateIconColor();
415                  invalidate();
416              }
417          });
418          animator.addListener(mCircleEndListener);
419          return animator;
420      }
421  
cancelAnimator(Animator animator)422      private void cancelAnimator(Animator animator) {
423          if (animator != null) {
424              animator.cancel();
425          }
426      }
427  
setImageScale(float imageScale, boolean animate)428      public void setImageScale(float imageScale, boolean animate) {
429          setImageScale(imageScale, animate, -1, null);
430      }
431  
432      /**
433       * Sets the scale of the containing image
434       *
435       * @param imageScale The new Scale.
436       * @param animate Should an animation be performed
437       * @param duration If animate, whats the duration? When -1 we take the default duration
438       * @param interpolator If animate, whats the interpolator? When null we take the default
439       *                     interpolator.
440       */
setImageScale(float imageScale, boolean animate, long duration, Interpolator interpolator)441      public void setImageScale(float imageScale, boolean animate, long duration,
442              Interpolator interpolator) {
443          cancelAnimator(mScaleAnimator);
444          if (!animate) {
445              mImageScale = imageScale;
446              invalidate();
447          } else {
448              ValueAnimator animator = ValueAnimator.ofFloat(mImageScale, imageScale);
449              mScaleAnimator = animator;
450              animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
451                  @Override
452                  public void onAnimationUpdate(ValueAnimator animation) {
453                      mImageScale = (float) animation.getAnimatedValue();
454                      invalidate();
455                  }
456              });
457              animator.addListener(mScaleEndListener);
458              if (interpolator == null) {
459                  interpolator = imageScale == 0.0f
460                          ? Interpolators.FAST_OUT_LINEAR_IN
461                          : Interpolators.LINEAR_OUT_SLOW_IN;
462              }
463              animator.setInterpolator(interpolator);
464              if (duration == -1) {
465                  float durationFactor = Math.abs(mImageScale - imageScale)
466                          / (1.0f - MIN_ICON_SCALE_AMOUNT);
467                  durationFactor = Math.min(1.0f, durationFactor);
468                  duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
469              }
470              animator.setDuration(duration);
471              animator.start();
472          }
473      }
474  
getRestingAlpha()475      public float getRestingAlpha() {
476          return mRestingAlpha;
477      }
478  
setImageAlpha(float alpha, boolean animate)479      public void setImageAlpha(float alpha, boolean animate) {
480          setImageAlpha(alpha, animate, -1, null, null);
481      }
482  
483      /**
484       * Sets the alpha of the containing image
485       *
486       * @param alpha The new alpha.
487       * @param animate Should an animation be performed
488       * @param duration If animate, whats the duration? When -1 we take the default duration
489       * @param interpolator If animate, whats the interpolator? When null we take the default
490       *                     interpolator.
491       */
setImageAlpha(float alpha, boolean animate, long duration, Interpolator interpolator, Runnable runnable)492      public void setImageAlpha(float alpha, boolean animate, long duration,
493              Interpolator interpolator, Runnable runnable) {
494          cancelAnimator(mAlphaAnimator);
495          alpha = mLaunchingAffordance ? 0 : alpha;
496          int endAlpha = (int) (alpha * 255);
497          final Drawable background = getBackground();
498          if (!animate) {
499              if (background != null) background.mutate().setAlpha(endAlpha);
500              setImageAlpha(endAlpha);
501          } else {
502              int currentAlpha = getImageAlpha();
503              ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha);
504              mAlphaAnimator = animator;
505              animator.addUpdateListener(animation -> {
506                  int alpha1 = (int) animation.getAnimatedValue();
507                  if (background != null) background.mutate().setAlpha(alpha1);
508                  setImageAlpha(alpha1);
509              });
510              animator.addListener(mAlphaEndListener);
511              if (interpolator == null) {
512                  interpolator = alpha == 0.0f
513                          ? Interpolators.FAST_OUT_LINEAR_IN
514                          : Interpolators.LINEAR_OUT_SLOW_IN;
515              }
516              animator.setInterpolator(interpolator);
517              if (duration == -1) {
518                  float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f;
519                  durationFactor = Math.min(1.0f, durationFactor);
520                  duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
521              }
522              animator.setDuration(duration);
523              if (runnable != null) {
524                  animator.addListener(getEndListener(runnable));
525              }
526              animator.start();
527          }
528      }
529  
isAnimatingAlpha()530      public boolean isAnimatingAlpha() {
531          return mAlphaAnimator != null;
532      }
533  
getEndListener(final Runnable runnable)534      private Animator.AnimatorListener getEndListener(final Runnable runnable) {
535          return new AnimatorListenerAdapter() {
536              boolean mCancelled;
537              @Override
538              public void onAnimationCancel(Animator animation) {
539                  mCancelled = true;
540              }
541  
542              @Override
543              public void onAnimationEnd(Animator animation) {
544                  if (!mCancelled) {
545                      runnable.run();
546                  }
547              }
548          };
549      }
550  
getCircleRadius()551      public float getCircleRadius() {
552          return mCircleRadius;
553      }
554  
555      @Override
performClick()556      public boolean performClick() {
557          if (isClickable()) {
558              return super.performClick();
559          } else {
560              return false;
561          }
562      }
563  
setLaunchingAffordance(boolean launchingAffordance)564      public void setLaunchingAffordance(boolean launchingAffordance) {
565          mLaunchingAffordance = launchingAffordance;
566      }
567  }
568