Home | History | Annotate | Download | only in policy
      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.policy;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ObjectAnimator;
     22 import android.content.Context;
     23 import android.graphics.Canvas;
     24 import android.graphics.CanvasProperty;
     25 import android.graphics.ColorFilter;
     26 import android.graphics.Paint;
     27 import android.graphics.PixelFormat;
     28 import android.graphics.drawable.Drawable;
     29 import android.os.Handler;
     30 import android.os.SystemProperties;
     31 import android.view.DisplayListCanvas;
     32 import android.view.RenderNodeAnimator;
     33 import android.view.View;
     34 import android.view.ViewConfiguration;
     35 import android.view.animation.Interpolator;
     36 
     37 import com.android.systemui.Interpolators;
     38 import com.android.systemui.R;
     39 
     40 import java.util.ArrayList;
     41 import java.util.HashSet;
     42 
     43 public class KeyButtonRipple extends Drawable {
     44 
     45     private static final float GLOW_MAX_SCALE_FACTOR = 1.35f;
     46     private static final float GLOW_MAX_ALPHA = 0.2f;
     47     private static final float GLOW_MAX_ALPHA_DARK = 0.1f;
     48     private static final int ANIMATION_DURATION_SCALE = 350;
     49     private static final int ANIMATION_DURATION_FADE = 450;
     50 
     51     private Paint mRipplePaint;
     52     private CanvasProperty<Float> mLeftProp;
     53     private CanvasProperty<Float> mTopProp;
     54     private CanvasProperty<Float> mRightProp;
     55     private CanvasProperty<Float> mBottomProp;
     56     private CanvasProperty<Float> mRxProp;
     57     private CanvasProperty<Float> mRyProp;
     58     private CanvasProperty<Paint> mPaintProp;
     59     private float mGlowAlpha = 0f;
     60     private float mGlowScale = 1f;
     61     private boolean mPressed;
     62     private boolean mVisible;
     63     private boolean mDrawingHardwareGlow;
     64     private int mMaxWidth;
     65     private boolean mLastDark;
     66     private boolean mDark;
     67     private boolean mDelayTouchFeedback;
     68 
     69     private final Interpolator mInterpolator = new LogInterpolator();
     70     private boolean mSupportHardware;
     71     private final View mTargetView;
     72     private final Handler mHandler = new Handler();
     73 
     74     private final HashSet<Animator> mRunningAnimations = new HashSet<>();
     75     private final ArrayList<Animator> mTmpArray = new ArrayList<>();
     76 
     77     public KeyButtonRipple(Context ctx, View targetView) {
     78         mMaxWidth =  ctx.getResources().getDimensionPixelSize(R.dimen.key_button_ripple_max_width);
     79         mTargetView = targetView;
     80     }
     81 
     82     public void setDarkIntensity(float darkIntensity) {
     83         mDark = darkIntensity >= 0.5f;
     84     }
     85 
     86     public void setDelayTouchFeedback(boolean delay) {
     87         mDelayTouchFeedback = delay;
     88     }
     89 
     90     private Paint getRipplePaint() {
     91         if (mRipplePaint == null) {
     92             mRipplePaint = new Paint();
     93             mRipplePaint.setAntiAlias(true);
     94             mRipplePaint.setColor(mLastDark ? 0xff000000 : 0xffffffff);
     95         }
     96         return mRipplePaint;
     97     }
     98 
     99     private void drawSoftware(Canvas canvas) {
    100         if (mGlowAlpha > 0f) {
    101             final Paint p = getRipplePaint();
    102             p.setAlpha((int)(mGlowAlpha * 255f));
    103 
    104             final float w = getBounds().width();
    105             final float h = getBounds().height();
    106             final boolean horizontal = w > h;
    107             final float diameter = getRippleSize() * mGlowScale;
    108             final float radius = diameter * .5f;
    109             final float cx = w * .5f;
    110             final float cy = h * .5f;
    111             final float rx = horizontal ? radius : cx;
    112             final float ry = horizontal ? cy : radius;
    113             final float corner = horizontal ? cy : cx;
    114 
    115             canvas.drawRoundRect(cx - rx, cy - ry,
    116                     cx + rx, cy + ry,
    117                     corner, corner, p);
    118         }
    119     }
    120 
    121     @Override
    122     public void draw(Canvas canvas) {
    123         mSupportHardware = canvas.isHardwareAccelerated();
    124         if (mSupportHardware) {
    125             drawHardware((DisplayListCanvas) canvas);
    126         } else {
    127             drawSoftware(canvas);
    128         }
    129     }
    130 
    131     @Override
    132     public void setAlpha(int alpha) {
    133         // Not supported.
    134     }
    135 
    136     @Override
    137     public void setColorFilter(ColorFilter colorFilter) {
    138         // Not supported.
    139     }
    140 
    141     @Override
    142     public int getOpacity() {
    143         return PixelFormat.TRANSLUCENT;
    144     }
    145 
    146     private boolean isHorizontal() {
    147         return getBounds().width() > getBounds().height();
    148     }
    149 
    150     private void drawHardware(DisplayListCanvas c) {
    151         if (mDrawingHardwareGlow) {
    152             c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp,
    153                     mPaintProp);
    154         }
    155     }
    156 
    157     public float getGlowAlpha() {
    158         return mGlowAlpha;
    159     }
    160 
    161     public void setGlowAlpha(float x) {
    162         mGlowAlpha = x;
    163         invalidateSelf();
    164     }
    165 
    166     public float getGlowScale() {
    167         return mGlowScale;
    168     }
    169 
    170     public void setGlowScale(float x) {
    171         mGlowScale = x;
    172         invalidateSelf();
    173     }
    174 
    175     private float getMaxGlowAlpha() {
    176         return mLastDark ? GLOW_MAX_ALPHA_DARK : GLOW_MAX_ALPHA;
    177     }
    178 
    179     @Override
    180     protected boolean onStateChange(int[] state) {
    181         boolean pressed = false;
    182         for (int i = 0; i < state.length; i++) {
    183             if (state[i] == android.R.attr.state_pressed) {
    184                 pressed = true;
    185                 break;
    186             }
    187         }
    188         if (pressed != mPressed) {
    189             setPressed(pressed);
    190             mPressed = pressed;
    191             return true;
    192         } else {
    193             return false;
    194         }
    195     }
    196 
    197     @Override
    198     public void jumpToCurrentState() {
    199         cancelAnimations();
    200     }
    201 
    202     @Override
    203     public boolean isStateful() {
    204         return true;
    205     }
    206 
    207     @Override
    208     public boolean hasFocusStateSpecified() {
    209         return true;
    210     }
    211 
    212     public void setPressed(boolean pressed) {
    213         if (mDark != mLastDark && pressed) {
    214             mRipplePaint = null;
    215             mLastDark = mDark;
    216         }
    217         if (mSupportHardware) {
    218             setPressedHardware(pressed);
    219         } else {
    220             setPressedSoftware(pressed);
    221         }
    222     }
    223 
    224     /**
    225      * Abort the ripple while it is delayed and before shown used only when setShouldDelayStartTouch
    226      * is enabled.
    227      */
    228     public void abortDelayedRipple() {
    229         mHandler.removeCallbacksAndMessages(null);
    230     }
    231 
    232     private void cancelAnimations() {
    233         mVisible = false;
    234         mTmpArray.addAll(mRunningAnimations);
    235         int size = mTmpArray.size();
    236         for (int i = 0; i < size; i++) {
    237             Animator a = mTmpArray.get(i);
    238             a.cancel();
    239         }
    240         mTmpArray.clear();
    241         mRunningAnimations.clear();
    242         mHandler.removeCallbacksAndMessages(null);
    243     }
    244 
    245     private void setPressedSoftware(boolean pressed) {
    246         if (pressed) {
    247             if (mDelayTouchFeedback) {
    248                 if (mRunningAnimations.isEmpty()) {
    249                     mHandler.removeCallbacksAndMessages(null);
    250                     mHandler.postDelayed(this::enterSoftware, ViewConfiguration.getTapTimeout());
    251                 } else if (mVisible) {
    252                     enterSoftware();
    253                 }
    254             } else {
    255                 enterSoftware();
    256             }
    257         } else {
    258             exitSoftware();
    259         }
    260     }
    261 
    262     private void enterSoftware() {
    263         cancelAnimations();
    264         mVisible = true;
    265         mGlowAlpha = getMaxGlowAlpha();
    266         ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale",
    267                 0f, GLOW_MAX_SCALE_FACTOR);
    268         scaleAnimator.setInterpolator(mInterpolator);
    269         scaleAnimator.setDuration(ANIMATION_DURATION_SCALE);
    270         scaleAnimator.addListener(mAnimatorListener);
    271         scaleAnimator.start();
    272         mRunningAnimations.add(scaleAnimator);
    273 
    274         // With the delay, it could eventually animate the enter animation with no pressed state,
    275         // then immediately show the exit animation. If this is skipped there will be no ripple.
    276         if (mDelayTouchFeedback && !mPressed) {
    277             exitSoftware();
    278         }
    279     }
    280 
    281     private void exitSoftware() {
    282         ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f);
    283         alphaAnimator.setInterpolator(Interpolators.ALPHA_OUT);
    284         alphaAnimator.setDuration(ANIMATION_DURATION_FADE);
    285         alphaAnimator.addListener(mAnimatorListener);
    286         alphaAnimator.start();
    287         mRunningAnimations.add(alphaAnimator);
    288     }
    289 
    290     private void setPressedHardware(boolean pressed) {
    291         if (pressed) {
    292             if (mDelayTouchFeedback) {
    293                 if (mRunningAnimations.isEmpty()) {
    294                     mHandler.removeCallbacksAndMessages(null);
    295                     mHandler.postDelayed(this::enterHardware, ViewConfiguration.getTapTimeout());
    296                 } else if (mVisible) {
    297                     enterHardware();
    298                 }
    299             } else {
    300                 enterHardware();
    301             }
    302         } else {
    303             exitHardware();
    304         }
    305     }
    306 
    307     /**
    308      * Sets the left/top property for the round rect to {@code prop} depending on whether we are
    309      * horizontal or vertical mode.
    310      */
    311     private void setExtendStart(CanvasProperty<Float> prop) {
    312         if (isHorizontal()) {
    313             mLeftProp = prop;
    314         } else {
    315             mTopProp = prop;
    316         }
    317     }
    318 
    319     private CanvasProperty<Float> getExtendStart() {
    320         return isHorizontal() ? mLeftProp : mTopProp;
    321     }
    322 
    323     /**
    324      * Sets the right/bottom property for the round rect to {@code prop} depending on whether we are
    325      * horizontal or vertical mode.
    326      */
    327     private void setExtendEnd(CanvasProperty<Float> prop) {
    328         if (isHorizontal()) {
    329             mRightProp = prop;
    330         } else {
    331             mBottomProp = prop;
    332         }
    333     }
    334 
    335     private CanvasProperty<Float> getExtendEnd() {
    336         return isHorizontal() ? mRightProp : mBottomProp;
    337     }
    338 
    339     private int getExtendSize() {
    340         return isHorizontal() ? getBounds().width() : getBounds().height();
    341     }
    342 
    343     private int getRippleSize() {
    344         int size = isHorizontal() ? getBounds().width() : getBounds().height();
    345         return Math.min(size, mMaxWidth);
    346     }
    347 
    348     private void enterHardware() {
    349         cancelAnimations();
    350         mVisible = true;
    351         mDrawingHardwareGlow = true;
    352         setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2));
    353         final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(),
    354                 getExtendSize()/2 - GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
    355         startAnim.setDuration(ANIMATION_DURATION_SCALE);
    356         startAnim.setInterpolator(mInterpolator);
    357         startAnim.addListener(mAnimatorListener);
    358         startAnim.setTarget(mTargetView);
    359 
    360         setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2));
    361         final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(),
    362                 getExtendSize()/2 + GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
    363         endAnim.setDuration(ANIMATION_DURATION_SCALE);
    364         endAnim.setInterpolator(mInterpolator);
    365         endAnim.addListener(mAnimatorListener);
    366         endAnim.setTarget(mTargetView);
    367 
    368         if (isHorizontal()) {
    369             mTopProp = CanvasProperty.createFloat(0f);
    370             mBottomProp = CanvasProperty.createFloat(getBounds().height());
    371             mRxProp = CanvasProperty.createFloat(getBounds().height()/2);
    372             mRyProp = CanvasProperty.createFloat(getBounds().height()/2);
    373         } else {
    374             mLeftProp = CanvasProperty.createFloat(0f);
    375             mRightProp = CanvasProperty.createFloat(getBounds().width());
    376             mRxProp = CanvasProperty.createFloat(getBounds().width()/2);
    377             mRyProp = CanvasProperty.createFloat(getBounds().width()/2);
    378         }
    379 
    380         mGlowScale = GLOW_MAX_SCALE_FACTOR;
    381         mGlowAlpha = getMaxGlowAlpha();
    382         mRipplePaint = getRipplePaint();
    383         mRipplePaint.setAlpha((int) (mGlowAlpha * 255));
    384         mPaintProp = CanvasProperty.createPaint(mRipplePaint);
    385 
    386         startAnim.start();
    387         endAnim.start();
    388         mRunningAnimations.add(startAnim);
    389         mRunningAnimations.add(endAnim);
    390 
    391         invalidateSelf();
    392 
    393         // With the delay, it could eventually animate the enter animation with no pressed state,
    394         // then immediately show the exit animation. If this is skipped there will be no ripple.
    395         if (mDelayTouchFeedback && !mPressed) {
    396             exitHardware();
    397         }
    398     }
    399 
    400     private void exitHardware() {
    401         mPaintProp = CanvasProperty.createPaint(getRipplePaint());
    402         final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp,
    403                 RenderNodeAnimator.PAINT_ALPHA, 0);
    404         opacityAnim.setDuration(ANIMATION_DURATION_FADE);
    405         opacityAnim.setInterpolator(Interpolators.ALPHA_OUT);
    406         opacityAnim.addListener(mAnimatorListener);
    407         opacityAnim.setTarget(mTargetView);
    408 
    409         opacityAnim.start();
    410         mRunningAnimations.add(opacityAnim);
    411 
    412         invalidateSelf();
    413     }
    414 
    415     private final AnimatorListenerAdapter mAnimatorListener =
    416             new AnimatorListenerAdapter() {
    417         @Override
    418         public void onAnimationEnd(Animator animation) {
    419             mRunningAnimations.remove(animation);
    420             if (mRunningAnimations.isEmpty() && !mPressed) {
    421                 mVisible = false;
    422                 mDrawingHardwareGlow = false;
    423                 invalidateSelf();
    424             }
    425         }
    426     };
    427 
    428     /**
    429      * Interpolator with a smooth log deceleration
    430      */
    431     private static final class LogInterpolator implements Interpolator {
    432         @Override
    433         public float getInterpolation(float input) {
    434             return 1 - (float) Math.pow(400, -input * 1.4);
    435         }
    436     }
    437 }
    438