1 /* 2 * Copyright (C) 2018 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.charging; 18 19 import android.animation.AnimatorSet; 20 import android.animation.ObjectAnimator; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.graphics.Color; 24 import android.util.AttributeSet; 25 import android.util.TypedValue; 26 import android.view.ContextThemeWrapper; 27 import android.view.View; 28 import android.view.animation.PathInterpolator; 29 import android.widget.FrameLayout; 30 import android.widget.ImageView; 31 import android.widget.TextView; 32 33 import com.android.settingslib.Utils; 34 import com.android.systemui.R; 35 import com.android.systemui.animation.Interpolators; 36 import com.android.systemui.shared.recents.utilities.Utilities; 37 import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig; 38 import com.android.systemui.surfaceeffects.ripple.RippleShader; 39 import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape; 40 import com.android.systemui.surfaceeffects.ripple.RippleView; 41 42 import java.text.NumberFormat; 43 44 /** 45 * @hide 46 */ 47 final class WirelessChargingLayout extends FrameLayout { 48 private static final long CIRCLE_RIPPLE_ANIMATION_DURATION = 1500; 49 private static final long ROUNDED_BOX_RIPPLE_ANIMATION_DURATION = 3000; 50 private static final int SCRIM_COLOR = 0x4C000000; 51 private static final int SCRIM_FADE_DURATION = 300; 52 private RippleView mRippleView; 53 // This is only relevant to the rounded box ripple. 54 private RippleShader.SizeAtProgress[] mSizeAtProgressArray; 55 WirelessChargingLayout(Context context, int transmittingBatteryLevel, int batteryLevel, boolean isDozing, RippleShape rippleShape)56 WirelessChargingLayout(Context context, int transmittingBatteryLevel, int batteryLevel, 57 boolean isDozing, RippleShape rippleShape) { 58 super(context); 59 init(context, null, transmittingBatteryLevel, batteryLevel, isDozing, rippleShape); 60 } 61 WirelessChargingLayout(Context context)62 private WirelessChargingLayout(Context context) { 63 super(context); 64 init(context, null, /* isDozing= */ false, RippleShape.CIRCLE); 65 } 66 WirelessChargingLayout(Context context, AttributeSet attrs)67 private WirelessChargingLayout(Context context, AttributeSet attrs) { 68 super(context, attrs); 69 init(context, attrs, /* isDozing= */false, RippleShape.CIRCLE); 70 } 71 init(Context c, AttributeSet attrs, boolean isDozing, RippleShape rippleShape)72 private void init(Context c, AttributeSet attrs, boolean isDozing, RippleShape rippleShape) { 73 init(c, attrs, -1, -1, isDozing, rippleShape); 74 } 75 init(Context context, AttributeSet attrs, int transmittingBatteryLevel, int batteryLevel, boolean isDozing, RippleShape rippleShape)76 private void init(Context context, AttributeSet attrs, int transmittingBatteryLevel, 77 int batteryLevel, boolean isDozing, RippleShape rippleShape) { 78 final boolean showTransmittingBatteryLevel = 79 (transmittingBatteryLevel != WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL); 80 81 // set style based on background 82 int style = R.style.ChargingAnim_WallpaperBackground; 83 if (isDozing) { 84 style = R.style.ChargingAnim_DarkBackground; 85 } 86 87 inflate(new ContextThemeWrapper(context, style), R.layout.wireless_charging_layout, this); 88 89 // amount of battery: 90 final TextView percentage = findViewById(R.id.wireless_charging_percentage); 91 92 if (batteryLevel != WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL) { 93 percentage.setText(NumberFormat.getPercentInstance().format(batteryLevel / 100f)); 94 percentage.setAlpha(0); 95 } 96 97 final long chargingAnimationFadeStartOffset = context.getResources().getInteger( 98 R.integer.wireless_charging_fade_offset); 99 final long chargingAnimationFadeDuration = context.getResources().getInteger( 100 R.integer.wireless_charging_fade_duration); 101 final float batteryLevelTextSizeStart = context.getResources().getFloat( 102 R.dimen.wireless_charging_anim_battery_level_text_size_start); 103 final float batteryLevelTextSizeEnd = context.getResources().getFloat( 104 R.dimen.wireless_charging_anim_battery_level_text_size_end) * ( 105 showTransmittingBatteryLevel ? 0.75f : 1.0f); 106 107 // Animation Scale: battery percentage text scales from 0% to 100% 108 ValueAnimator textSizeAnimator = ObjectAnimator.ofFloat(percentage, "textSize", 109 batteryLevelTextSizeStart, batteryLevelTextSizeEnd); 110 textSizeAnimator.setInterpolator(new PathInterpolator(0, 0, 0, 1)); 111 textSizeAnimator.setDuration(context.getResources().getInteger( 112 R.integer.wireless_charging_battery_level_text_scale_animation_duration)); 113 114 // Animation Opacity: battery percentage text transitions from 0 to 1 opacity 115 ValueAnimator textOpacityAnimator = ObjectAnimator.ofFloat(percentage, "alpha", 0, 1); 116 textOpacityAnimator.setInterpolator(Interpolators.LINEAR); 117 textOpacityAnimator.setDuration(context.getResources().getInteger( 118 R.integer.wireless_charging_battery_level_text_opacity_duration)); 119 textOpacityAnimator.setStartDelay(context.getResources().getInteger( 120 R.integer.wireless_charging_anim_opacity_offset)); 121 122 // Animation Opacity: battery percentage text fades from 1 to 0 opacity 123 ValueAnimator textFadeAnimator = ObjectAnimator.ofFloat(percentage, "alpha", 1, 0); 124 textFadeAnimator.setDuration(chargingAnimationFadeDuration); 125 textFadeAnimator.setInterpolator(Interpolators.LINEAR); 126 textFadeAnimator.setStartDelay(chargingAnimationFadeStartOffset); 127 128 // play all animations together 129 AnimatorSet animatorSet = new AnimatorSet(); 130 animatorSet.playTogether(textSizeAnimator, textOpacityAnimator, textFadeAnimator); 131 132 // For large screens docking animation, we don't play the background scrim. 133 if (!Utilities.isLargeScreen(context)) { 134 ValueAnimator scrimFadeInAnimator = ObjectAnimator.ofArgb(this, 135 "backgroundColor", Color.TRANSPARENT, SCRIM_COLOR); 136 scrimFadeInAnimator.setDuration(SCRIM_FADE_DURATION); 137 scrimFadeInAnimator.setInterpolator(Interpolators.LINEAR); 138 ValueAnimator scrimFadeOutAnimator = ObjectAnimator.ofArgb(this, 139 "backgroundColor", SCRIM_COLOR, Color.TRANSPARENT); 140 scrimFadeOutAnimator.setDuration(SCRIM_FADE_DURATION); 141 scrimFadeOutAnimator.setInterpolator(Interpolators.LINEAR); 142 scrimFadeOutAnimator.setStartDelay((rippleShape == RippleShape.CIRCLE 143 ? CIRCLE_RIPPLE_ANIMATION_DURATION : ROUNDED_BOX_RIPPLE_ANIMATION_DURATION) 144 - SCRIM_FADE_DURATION); 145 AnimatorSet animatorSetScrim = new AnimatorSet(); 146 animatorSetScrim.playTogether(scrimFadeInAnimator, scrimFadeOutAnimator); 147 animatorSetScrim.start(); 148 } 149 150 mRippleView = findViewById(R.id.wireless_charging_ripple); 151 mRippleView.setupShader(rippleShape); 152 int color = Utils.getColorAttr(mRippleView.getContext(), 153 android.R.attr.colorAccent).getDefaultColor(); 154 if (mRippleView.getRippleShape() == RippleShape.ROUNDED_BOX) { 155 mRippleView.setDuration(ROUNDED_BOX_RIPPLE_ANIMATION_DURATION); 156 mRippleView.setSparkleStrength(0.22f); 157 mRippleView.setColor(color, 102); // 40% of opacity. 158 mRippleView.setBaseRingFadeParams( 159 /* fadeInStart = */ 0f, 160 /* fadeInEnd = */ 0f, 161 /* fadeOutStart = */ 0.2f, 162 /* fadeOutEnd= */ 0.47f 163 ); 164 mRippleView.setSparkleRingFadeParams( 165 /* fadeInStart = */ 0f, 166 /* fadeInEnd = */ 0f, 167 /* fadeOutStart = */ 0.2f, 168 /* fadeOutEnd= */ 1f 169 ); 170 mRippleView.setCenterFillFadeParams( 171 /* fadeInStart = */ 0f, 172 /* fadeInEnd = */ 0f, 173 /* fadeOutStart = */ 0f, 174 /* fadeOutEnd= */ 0.2f 175 ); 176 mRippleView.setBlur(6.5f, 2.5f); 177 } else { 178 mRippleView.setDuration(CIRCLE_RIPPLE_ANIMATION_DURATION); 179 mRippleView.setColor(color, RippleAnimationConfig.RIPPLE_DEFAULT_ALPHA); 180 } 181 182 OnAttachStateChangeListener listener = new OnAttachStateChangeListener() { 183 @Override 184 public void onViewAttachedToWindow(View view) { 185 mRippleView.startRipple(); 186 mRippleView.removeOnAttachStateChangeListener(this); 187 } 188 189 @Override 190 public void onViewDetachedFromWindow(View view) {} 191 }; 192 mRippleView.addOnAttachStateChangeListener(listener); 193 194 if (!showTransmittingBatteryLevel) { 195 animatorSet.start(); 196 return; 197 } 198 199 // amount of transmitting battery: 200 final TextView transmittingPercentage = findViewById( 201 R.id.reverse_wireless_charging_percentage); 202 transmittingPercentage.setVisibility(VISIBLE); 203 transmittingPercentage.setText( 204 NumberFormat.getPercentInstance().format(transmittingBatteryLevel / 100f)); 205 transmittingPercentage.setAlpha(0); 206 207 // Animation Scale: transmitting battery percentage text scales from 0% to 100% 208 ValueAnimator textSizeAnimatorTransmitting = ObjectAnimator.ofFloat(transmittingPercentage, 209 "textSize", batteryLevelTextSizeStart, batteryLevelTextSizeEnd); 210 textSizeAnimatorTransmitting.setInterpolator(new PathInterpolator(0, 0, 0, 1)); 211 textSizeAnimatorTransmitting.setDuration(context.getResources().getInteger( 212 R.integer.wireless_charging_battery_level_text_scale_animation_duration)); 213 214 // Animation Opacity: transmitting battery percentage text transitions from 0 to 1 opacity 215 ValueAnimator textOpacityAnimatorTransmitting = ObjectAnimator.ofFloat( 216 transmittingPercentage, "alpha", 0, 1); 217 textOpacityAnimatorTransmitting.setInterpolator(Interpolators.LINEAR); 218 textOpacityAnimatorTransmitting.setDuration(context.getResources().getInteger( 219 R.integer.wireless_charging_battery_level_text_opacity_duration)); 220 textOpacityAnimatorTransmitting.setStartDelay( 221 context.getResources().getInteger(R.integer.wireless_charging_anim_opacity_offset)); 222 223 // Animation Opacity: transmitting battery percentage text fades from 1 to 0 opacity 224 ValueAnimator textFadeAnimatorTransmitting = ObjectAnimator.ofFloat(transmittingPercentage, 225 "alpha", 1, 0); 226 textFadeAnimatorTransmitting.setDuration(chargingAnimationFadeDuration); 227 textFadeAnimatorTransmitting.setInterpolator(Interpolators.LINEAR); 228 textFadeAnimatorTransmitting.setStartDelay(chargingAnimationFadeStartOffset); 229 230 // play all animations together 231 AnimatorSet animatorSetTransmitting = new AnimatorSet(); 232 animatorSetTransmitting.playTogether(textSizeAnimatorTransmitting, 233 textOpacityAnimatorTransmitting, textFadeAnimatorTransmitting); 234 235 // transmitting battery icon 236 final ImageView chargingViewIcon = findViewById(R.id.reverse_wireless_charging_icon); 237 chargingViewIcon.setVisibility(VISIBLE); 238 final int padding = Math.round( 239 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, batteryLevelTextSizeEnd, 240 getResources().getDisplayMetrics())); 241 chargingViewIcon.setPadding(padding, 0, padding, 0); 242 243 // Animation Opacity: transmitting battery icon transitions from 0 to 1 opacity 244 ValueAnimator textOpacityAnimatorIcon = ObjectAnimator.ofFloat(chargingViewIcon, "alpha", 0, 245 1); 246 textOpacityAnimatorIcon.setInterpolator(Interpolators.LINEAR); 247 textOpacityAnimatorIcon.setDuration(context.getResources().getInteger( 248 R.integer.wireless_charging_battery_level_text_opacity_duration)); 249 textOpacityAnimatorIcon.setStartDelay( 250 context.getResources().getInteger(R.integer.wireless_charging_anim_opacity_offset)); 251 252 // Animation Opacity: transmitting battery icon fades from 1 to 0 opacity 253 ValueAnimator textFadeAnimatorIcon = ObjectAnimator.ofFloat(chargingViewIcon, "alpha", 1, 254 0); 255 textFadeAnimatorIcon.setDuration(chargingAnimationFadeDuration); 256 textFadeAnimatorIcon.setInterpolator(Interpolators.LINEAR); 257 textFadeAnimatorIcon.setStartDelay(chargingAnimationFadeStartOffset); 258 259 // play all animations together 260 AnimatorSet animatorSetIcon = new AnimatorSet(); 261 animatorSetIcon.playTogether(textOpacityAnimatorIcon, textFadeAnimatorIcon); 262 263 animatorSet.start(); 264 animatorSetTransmitting.start(); 265 animatorSetIcon.start(); 266 } 267 268 @Override onLayout(boolean changed, int left, int top, int right, int bottom)269 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 270 if (mRippleView != null) { 271 int width = getMeasuredWidth(); 272 int height = getMeasuredHeight(); 273 mRippleView.setCenter(width * 0.5f, height * 0.5f); 274 if (mRippleView.getRippleShape() == RippleShape.ROUNDED_BOX) { 275 updateRippleSizeAtProgressList(width, height); 276 } else { 277 float maxSize = Math.max(width, height); 278 mRippleView.setMaxSize(maxSize, maxSize); 279 } 280 } 281 282 super.onLayout(changed, left, top, right, bottom); 283 } 284 updateRippleSizeAtProgressList(float width, float height)285 private void updateRippleSizeAtProgressList(float width, float height) { 286 if (mSizeAtProgressArray == null) { 287 float maxSize = Math.max(width, height); 288 mSizeAtProgressArray = new RippleShader.SizeAtProgress[] { 289 // Those magic numbers are introduced for visual polish. It starts from a pill 290 // shape and expand to a full circle. 291 new RippleShader.SizeAtProgress(0f, 0f, 0f), 292 new RippleShader.SizeAtProgress(0.3f, width * 0.4f, height * 0.4f), 293 new RippleShader.SizeAtProgress(1f, maxSize, maxSize) 294 }; 295 } else { 296 // Same multipliers, just need to recompute with the new width and height. 297 RippleShader.SizeAtProgress first = mSizeAtProgressArray[0]; 298 first.setT(0f); 299 first.setWidth(0f); 300 first.setHeight(0f); 301 302 RippleShader.SizeAtProgress second = mSizeAtProgressArray[1]; 303 second.setT(0.3f); 304 second.setWidth(width * 0.4f); 305 second.setHeight(height * 0.4f); 306 307 float maxSize = Math.max(width, height); 308 RippleShader.SizeAtProgress last = mSizeAtProgressArray[2]; 309 last.setT(1f); 310 last.setWidth(maxSize); 311 last.setHeight(maxSize); 312 } 313 314 mRippleView.setSizeAtProgresses(mSizeAtProgressArray); 315 } 316 } 317