1 /* 2 * Copyright (C) 2023 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.tv.privacy; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorInflater; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Canvas; 24 import android.graphics.ColorFilter; 25 import android.graphics.Paint; 26 import android.graphics.Path; 27 import android.graphics.PixelFormat; 28 import android.graphics.Rect; 29 import android.graphics.RectF; 30 import android.graphics.drawable.Drawable; 31 import android.util.Log; 32 import android.util.MathUtils; 33 import android.view.Gravity; 34 35 import androidx.annotation.Keep; 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 39 import com.android.systemui.tv.res.R; 40 41 /** 42 * Drawable that can go from being the background of the privacy icons to a small dot. 43 * The icons are not included. 44 */ 45 public class PrivacyChipDrawable extends Drawable { 46 private static final String TAG = PrivacyChipDrawable.class.getSimpleName(); 47 private static final boolean DEBUG = false; 48 49 private final Paint mChipPaint; 50 private final Paint mBgPaint; 51 private final Rect mTmpRect = new Rect(); 52 private final Rect mBgRect = new Rect(); 53 private final RectF mTmpRectF = new RectF(); 54 private final Path mPath = new Path(); 55 private final Animator mCollapse; 56 private final Animator mExpand; 57 private final int mLayoutDirection; 58 private final int mBgWidth; 59 private final int mBgHeight; 60 private final int mBgRadius; 61 private final int mDotSize; 62 private final float mExpandedChipRadius; 63 private final float mCollapsedChipRadius; 64 65 private final boolean mCollapseToDot; 66 67 private boolean mIsExpanded = true; 68 private float mCollapseProgress = 0f; 69 PrivacyChipDrawable(Context context, int chipColorRes, boolean collapseToDot)70 public PrivacyChipDrawable(Context context, int chipColorRes, boolean collapseToDot) { 71 mCollapseToDot = collapseToDot; 72 73 mChipPaint = new Paint(); 74 mChipPaint.setStyle(Paint.Style.FILL); 75 mChipPaint.setColor(context.getColor(chipColorRes)); 76 mChipPaint.setFlags(Paint.ANTI_ALIAS_FLAG); 77 78 mBgPaint = new Paint(); 79 mBgPaint.setStyle(Paint.Style.FILL); 80 mBgPaint.setColor(context.getColor(R.color.privacy_chip_dot_bg_tint)); 81 mBgPaint.setFlags(Paint.ANTI_ALIAS_FLAG); 82 83 Resources res = context.getResources(); 84 mLayoutDirection = res.getConfiguration().getLayoutDirection(); 85 mBgWidth = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_bg_width); 86 mBgHeight = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_bg_height); 87 mBgRadius = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_bg_radius); 88 mDotSize = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_size); 89 90 mExpandedChipRadius = res.getDimensionPixelSize(R.dimen.privacy_chip_radius); 91 mCollapsedChipRadius = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_radius); 92 93 mExpand = AnimatorInflater.loadAnimator(context, R.anim.privacy_chip_expand); 94 mExpand.setTarget(this); 95 mCollapse = AnimatorInflater.loadAnimator(context, R.anim.privacy_chip_collapse); 96 mCollapse.setTarget(this); 97 } 98 99 /** 100 * @return how far the chip is currently collapsed. 101 * @see #setCollapseProgress(float) 102 */ 103 @Keep getCollapseProgress()104 public float getCollapseProgress() { 105 return mCollapseProgress; 106 } 107 108 /** 109 * Sets the collapsing progress of the chip to its collapsed state. 110 * @param pct How far the chip is collapsed, in the range 0-1. 111 * 0=fully expanded, 1=fully collapsed. 112 */ 113 @Keep setCollapseProgress(float pct)114 public void setCollapseProgress(float pct) { 115 mCollapseProgress = pct; 116 invalidateSelf(); 117 } 118 119 @Override draw(@onNull Canvas canvas)120 public void draw(@NonNull Canvas canvas) { 121 if (mCollapseProgress > 0f) { 122 // draw background 123 getBackgroundBounds(mBgRect); 124 mTmpRectF.set(mBgRect); 125 canvas.drawRoundRect(mTmpRectF, mBgRadius, mBgRadius, mBgPaint); 126 } 127 128 getForegroundBounds(mTmpRectF); 129 float radius = MathUtils.lerp( 130 mExpandedChipRadius, 131 mCollapseToDot ? mCollapsedChipRadius : mBgRadius, 132 mCollapseProgress); 133 134 canvas.drawRoundRect(mTmpRectF, radius, radius, mChipPaint); 135 } 136 getBackgroundBounds(Rect out)137 private void getBackgroundBounds(Rect out) { 138 Rect bounds = getBounds(); 139 Gravity.apply(Gravity.END, mBgWidth, mBgHeight, bounds, out, mLayoutDirection); 140 } 141 getCollapsedForegroundBounds(Rect out)142 private void getCollapsedForegroundBounds(Rect out) { 143 Rect bounds = getBounds(); 144 getBackgroundBounds(mBgRect); 145 if (mCollapseToDot) { 146 Gravity.apply(Gravity.CENTER, mDotSize, mDotSize, mBgRect, out); 147 } else { 148 out.set(bounds.left, mBgRect.top, bounds.right, mBgRect.bottom); 149 } 150 } 151 getForegroundBounds(RectF out)152 private void getForegroundBounds(RectF out) { 153 Rect bounds = getBounds(); 154 getCollapsedForegroundBounds(mTmpRect); 155 lerpRect(bounds, mTmpRect, mCollapseProgress, out); 156 } 157 lerpRect(Rect start, Rect stop, float amount, RectF out)158 private void lerpRect(Rect start, Rect stop, float amount, RectF out) { 159 float left = MathUtils.lerp(start.left, stop.left, amount); 160 float top = MathUtils.lerp(start.top, stop.top, amount); 161 float right = MathUtils.lerp(start.right, stop.right, amount); 162 float bottom = MathUtils.lerp(start.bottom, stop.bottom, amount); 163 out.set(left, top, right, bottom); 164 } 165 166 /** 167 * Clips the given canvas to this chip's foreground shape. 168 * @param canvas Canvas to clip. 169 */ clipToForeground(Canvas canvas)170 public void clipToForeground(Canvas canvas) { 171 getForegroundBounds(mTmpRectF); 172 float radius = MathUtils.lerp( 173 mExpandedChipRadius, 174 mCollapseToDot ? mCollapsedChipRadius : mBgRadius, 175 mCollapseProgress); 176 177 mPath.reset(); 178 mPath.addRoundRect(mTmpRectF, radius, radius, Path.Direction.CW); 179 canvas.clipPath(mPath); 180 } 181 182 @Override onBoundsChange(@onNull Rect bounds)183 protected void onBoundsChange(@NonNull Rect bounds) { 184 super.onBoundsChange(bounds); 185 invalidateSelf(); 186 } 187 188 @Override setAlpha(int alpha)189 public void setAlpha(int alpha) { 190 mChipPaint.setAlpha(alpha); 191 mBgPaint.setAlpha(alpha); 192 } 193 194 /** 195 * Transitions to a full chip. 196 * 197 * @param animate Whether to animate the change to a full chip, or expand instantly. 198 */ expand(boolean animate)199 public void expand(boolean animate) { 200 if (DEBUG) Log.d(TAG, "expanding"); 201 if (mIsExpanded) { 202 return; 203 } 204 mIsExpanded = true; 205 if (animate) { 206 mCollapse.cancel(); 207 mExpand.start(); 208 } else { 209 mCollapseProgress = 0f; 210 invalidateSelf(); 211 } 212 } 213 214 /** 215 * Starts the animation to a dot. 216 */ collapse()217 public void collapse() { 218 if (DEBUG) Log.d(TAG, "collapsing"); 219 if (!mIsExpanded) { 220 return; 221 } 222 mIsExpanded = false; 223 mExpand.cancel(); 224 mCollapse.start(); 225 } 226 227 @Override setColorFilter(@ullable ColorFilter colorFilter)228 public void setColorFilter(@Nullable ColorFilter colorFilter) { 229 // no-op 230 } 231 232 @Override getOpacity()233 public int getOpacity() { 234 return PixelFormat.TRANSLUCENT; 235 } 236 } 237