1 /* 2 * Copyright (C) 2020 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.navigationbar.gestural; 18 19 import android.animation.ArgbEvaluator; 20 import android.animation.ObjectAnimator; 21 import android.annotation.ColorInt; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.graphics.Canvas; 25 import android.graphics.Paint; 26 import android.graphics.drawable.Drawable; 27 import android.util.AttributeSet; 28 import android.util.FloatProperty; 29 import android.view.ContextThemeWrapper; 30 import android.view.View; 31 import android.view.animation.Interpolator; 32 33 import com.android.app.animation.Interpolators; 34 import com.android.settingslib.Utils; 35 import com.android.systemui.navigationbar.buttons.ButtonInterface; 36 import com.android.systemui.res.R; 37 38 public class NavigationHandle extends View implements ButtonInterface { 39 40 protected final Paint mPaint = new Paint(); 41 private @ColorInt final int mLightColor; 42 private @ColorInt final int mDarkColor; 43 protected final float mRadius; 44 protected final float mBottom; 45 private final float mAdditionalWidthForAnimation; 46 private final float mAdditionalHeightForAnimation; 47 private final float mShrinkWidthForAnimation; 48 private boolean mRequiresInvalidate; 49 private boolean mShrink; 50 51 private ObjectAnimator mPulseAnimator = null; 52 private float mPulseAnimationProgress; 53 54 private static final FloatProperty<NavigationHandle> PULSE_ANIMATION_PROGRESS = 55 new FloatProperty<>("pulseAnimationProgress") { 56 @Override 57 public Float get(NavigationHandle controller) { 58 return controller.getPulseAnimationProgress(); 59 } 60 61 @Override 62 public void setValue(NavigationHandle controller, float progress) { 63 controller.setPulseAnimationProgress(progress); 64 } 65 }; 66 NavigationHandle(Context context)67 public NavigationHandle(Context context) { 68 this(context, null); 69 } 70 NavigationHandle(Context context, AttributeSet attr)71 public NavigationHandle(Context context, AttributeSet attr) { 72 super(context, attr); 73 final Resources res = context.getResources(); 74 mRadius = res.getDimension(R.dimen.navigation_handle_radius); 75 mBottom = res.getDimension(R.dimen.navigation_handle_bottom); 76 mAdditionalWidthForAnimation = 77 res.getDimension(R.dimen.navigation_home_handle_additional_width_for_animation); 78 mAdditionalHeightForAnimation = 79 res.getDimension(R.dimen.navigation_home_handle_additional_height_for_animation); 80 mShrinkWidthForAnimation = 81 res.getDimension(R.dimen.navigation_home_handle_shrink_width_for_animation); 82 83 final int dualToneDarkTheme = Utils.getThemeAttr(context, R.attr.darkIconTheme); 84 final int dualToneLightTheme = Utils.getThemeAttr(context, R.attr.lightIconTheme); 85 Context lightContext = new ContextThemeWrapper(context, dualToneLightTheme); 86 Context darkContext = new ContextThemeWrapper(context, dualToneDarkTheme); 87 mLightColor = Utils.getColorAttrDefaultColor(lightContext, R.attr.homeHandleColor); 88 mDarkColor = Utils.getColorAttrDefaultColor(darkContext, R.attr.homeHandleColor); 89 mPaint.setAntiAlias(true); 90 setFocusable(false); 91 } 92 93 @Override setAlpha(float alpha)94 public void setAlpha(float alpha) { 95 super.setAlpha(alpha); 96 if (alpha > 0f && mRequiresInvalidate) { 97 mRequiresInvalidate = false; 98 invalidate(); 99 } 100 } 101 102 @Override onDraw(Canvas canvas)103 protected void onDraw(Canvas canvas) { 104 super.onDraw(canvas); 105 106 // Draw that bar 107 int navHeight = getHeight(); 108 float additionalHeight; 109 float additionalWidth; 110 if (mShrink) { 111 additionalHeight = 0; 112 additionalWidth = -mShrinkWidthForAnimation * mPulseAnimationProgress; 113 } else { 114 additionalHeight = mAdditionalHeightForAnimation * mPulseAnimationProgress; 115 additionalWidth = mAdditionalWidthForAnimation * mPulseAnimationProgress; 116 } 117 118 float height = mRadius * 2 + additionalHeight; 119 float width = getWidth() + additionalWidth; 120 float x = -additionalWidth; 121 float y = navHeight - mBottom - height + (additionalHeight / 2); 122 float adjustedRadius = height / 2; 123 canvas.drawRoundRect(x, y, width, y + height, adjustedRadius, adjustedRadius, mPaint); 124 } 125 126 @Override setImageDrawable(Drawable drawable)127 public void setImageDrawable(Drawable drawable) {} 128 129 @Override abortCurrentGesture()130 public void abortCurrentGesture() {} 131 132 @Override setVertical(boolean vertical)133 public void setVertical(boolean vertical) {} 134 135 @Override setDarkIntensity(float intensity)136 public void setDarkIntensity(float intensity) { 137 int color = (int) ArgbEvaluator.getInstance().evaluate(intensity, mLightColor, mDarkColor); 138 if (mPaint.getColor() != color) { 139 mPaint.setColor(color); 140 if (getVisibility() == VISIBLE && getAlpha() > 0) { 141 invalidate(); 142 } else { 143 // If we are currently invisible, then invalidate when we are next made visible 144 mRequiresInvalidate = true; 145 } 146 } 147 } 148 149 @Override setDelayTouchFeedback(boolean shouldDelay)150 public void setDelayTouchFeedback(boolean shouldDelay) {} 151 152 @Override animateLongPress(boolean isTouchDown, boolean shrink, long durationMs)153 public void animateLongPress(boolean isTouchDown, boolean shrink, long durationMs) { 154 if (mPulseAnimator != null) { 155 mPulseAnimator.cancel(); 156 } 157 158 mShrink = shrink; 159 Interpolator interpolator; 160 if (shrink) { 161 interpolator = Interpolators.LEGACY_DECELERATE; 162 } else { 163 if (isTouchDown) { 164 // For now we animate the navbar expanding and contracting so that the navbar is 165 // the original size by the end of {@code duration}. This is because a screenshot 166 // is taken at that point and we don't want to capture the larger navbar. 167 // TODO(b/306400785): Determine a way to exclude navbar from the screenshot. 168 169 // Fraction of the touch down animation to expand; remaining is used to contract 170 // again. 171 float expandFraction = 0.9f; 172 interpolator = t -> t <= expandFraction 173 ? Interpolators.clampToProgress(Interpolators.LEGACY, t, 0, expandFraction) 174 : 1 - Interpolators.clampToProgress( 175 Interpolators.LINEAR, t, expandFraction, 1); 176 } else { 177 interpolator = Interpolators.LEGACY_DECELERATE; 178 } 179 } 180 181 mPulseAnimator = 182 ObjectAnimator.ofFloat(this, PULSE_ANIMATION_PROGRESS, isTouchDown ? 1 : 0); 183 mPulseAnimator.setDuration(durationMs).setInterpolator(interpolator); 184 mPulseAnimator.start(); 185 } 186 setPulseAnimationProgress(float pulseAnimationProgress)187 private void setPulseAnimationProgress(float pulseAnimationProgress) { 188 mPulseAnimationProgress = pulseAnimationProgress; 189 invalidate(); 190 } 191 getPulseAnimationProgress()192 private float getPulseAnimationProgress() { 193 return mPulseAnimationProgress; 194 } 195 } 196