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.shared.rotation; 18 19 import android.annotation.DimenRes; 20 import android.annotation.IdRes; 21 import android.annotation.LayoutRes; 22 import android.annotation.StringRes; 23 import android.content.Context; 24 import android.content.pm.ActivityInfo; 25 import android.content.pm.ActivityInfo.Config; 26 import android.content.res.Resources; 27 import android.graphics.PixelFormat; 28 import android.graphics.drawable.AnimatedVectorDrawable; 29 import android.graphics.drawable.Drawable; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.WindowManager; 34 import android.view.WindowManager.LayoutParams; 35 import android.view.animation.AccelerateDecelerateInterpolator; 36 import android.widget.FrameLayout; 37 38 import androidx.annotation.BoolRes; 39 import androidx.core.view.OneShotPreDrawListener; 40 41 import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position; 42 43 /** 44 * Containing logic for the rotation button on the physical left bottom corner of the screen. 45 */ 46 public class FloatingRotationButton implements RotationButton { 47 48 private static final int MARGIN_ANIMATION_DURATION_MILLIS = 300; 49 50 private final WindowManager mWindowManager; 51 private final ViewGroup mKeyButtonContainer; 52 private final FloatingRotationButtonView mKeyButtonView; 53 54 private int mContainerSize; 55 private final Context mContext; 56 57 @StringRes 58 private final int mContentDescriptionResource; 59 @DimenRes 60 private final int mMinMarginResource; 61 @DimenRes 62 private final int mRoundedContentPaddingResource; 63 @DimenRes 64 private final int mTaskbarLeftMarginResource; 65 @DimenRes 66 private final int mTaskbarBottomMarginResource; 67 @DimenRes 68 private final int mButtonDiameterResource; 69 @BoolRes 70 private final int mFloatingRotationBtnPositionLeftResource; 71 72 private AnimatedVectorDrawable mAnimatedDrawable; 73 private boolean mIsShowing; 74 private int mDisplayRotation; 75 76 private boolean mIsTaskbarVisible = false; 77 private boolean mIsTaskbarStashed = false; 78 79 private FloatingRotationButtonPositionCalculator mPositionCalculator; 80 81 private RotationButtonController mRotationButtonController; 82 private RotationButtonUpdatesCallback mUpdatesCallback; 83 private Position mPosition; 84 FloatingRotationButton(Context context, @StringRes int contentDescriptionResource, @LayoutRes int layout, @IdRes int keyButtonId, @DimenRes int minMargin, @DimenRes int roundedContentPadding, @DimenRes int taskbarLeftMargin, @DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter, @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource)85 public FloatingRotationButton(Context context, @StringRes int contentDescriptionResource, 86 @LayoutRes int layout, @IdRes int keyButtonId, @DimenRes int minMargin, 87 @DimenRes int roundedContentPadding, @DimenRes int taskbarLeftMargin, 88 @DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter, 89 @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) { 90 mWindowManager = context.getSystemService(WindowManager.class); 91 mKeyButtonContainer = (ViewGroup) LayoutInflater.from(context).inflate(layout, null); 92 mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId); 93 mKeyButtonView.setVisibility(View.VISIBLE); 94 mKeyButtonView.setContentDescription(context.getString(contentDescriptionResource)); 95 mKeyButtonView.setRipple(rippleMaxWidth); 96 97 mContext = context; 98 99 mContentDescriptionResource = contentDescriptionResource; 100 mMinMarginResource = minMargin; 101 mRoundedContentPaddingResource = roundedContentPadding; 102 mTaskbarLeftMarginResource = taskbarLeftMargin; 103 mTaskbarBottomMarginResource = taskbarBottomMargin; 104 mButtonDiameterResource = buttonDiameter; 105 mFloatingRotationBtnPositionLeftResource = floatingRotationBtnPositionLeftResource; 106 107 updateDimensionResources(); 108 } 109 updateDimensionResources()110 private void updateDimensionResources() { 111 Resources res = mContext.getResources(); 112 113 int defaultMargin = Math.max( 114 res.getDimensionPixelSize(mMinMarginResource), 115 res.getDimensionPixelSize(mRoundedContentPaddingResource)); 116 117 int taskbarMarginLeft = 118 res.getDimensionPixelSize(mTaskbarLeftMarginResource); 119 int taskbarMarginBottom = 120 res.getDimensionPixelSize(mTaskbarBottomMarginResource); 121 122 boolean floatingRotationButtonPositionLeft = 123 res.getBoolean(mFloatingRotationBtnPositionLeftResource); 124 125 mPositionCalculator = new FloatingRotationButtonPositionCalculator(defaultMargin, 126 taskbarMarginLeft, taskbarMarginBottom, floatingRotationButtonPositionLeft); 127 128 final int diameter = res.getDimensionPixelSize(mButtonDiameterResource); 129 mContainerSize = diameter + Math.max(defaultMargin, Math.max(taskbarMarginLeft, 130 taskbarMarginBottom)); 131 } 132 133 @Override setRotationButtonController(RotationButtonController rotationButtonController)134 public void setRotationButtonController(RotationButtonController rotationButtonController) { 135 mRotationButtonController = rotationButtonController; 136 updateIcon(mRotationButtonController.getLightIconColor(), 137 mRotationButtonController.getDarkIconColor()); 138 } 139 140 @Override setUpdatesCallback(RotationButtonUpdatesCallback updatesCallback)141 public void setUpdatesCallback(RotationButtonUpdatesCallback updatesCallback) { 142 mUpdatesCallback = updatesCallback; 143 } 144 145 @Override getCurrentView()146 public View getCurrentView() { 147 return mKeyButtonView; 148 } 149 150 @Override show()151 public boolean show() { 152 if (mIsShowing) { 153 return false; 154 } 155 156 mIsShowing = true; 157 158 final LayoutParams layoutParams = adjustViewPositionAndCreateLayoutParams(); 159 mWindowManager.addView(mKeyButtonContainer, layoutParams); 160 161 if (mAnimatedDrawable != null) { 162 mAnimatedDrawable.reset(); 163 mAnimatedDrawable.start(); 164 } 165 166 // Notify about visibility only after first traversal so we can properly calculate 167 // the touch region for the button 168 OneShotPreDrawListener.add(mKeyButtonView, () -> { 169 if (mIsShowing && mUpdatesCallback != null) { 170 mUpdatesCallback.onVisibilityChanged(true); 171 } 172 }); 173 174 return true; 175 } 176 177 @Override hide()178 public boolean hide() { 179 if (!mIsShowing) { 180 return false; 181 } 182 mWindowManager.removeViewImmediate(mKeyButtonContainer); 183 mIsShowing = false; 184 if (mUpdatesCallback != null) { 185 mUpdatesCallback.onVisibilityChanged(false); 186 } 187 return true; 188 } 189 190 @Override isVisible()191 public boolean isVisible() { 192 return mIsShowing; 193 } 194 195 @Override updateIcon(int lightIconColor, int darkIconColor)196 public void updateIcon(int lightIconColor, int darkIconColor) { 197 mAnimatedDrawable = (AnimatedVectorDrawable) mKeyButtonView.getContext() 198 .getDrawable(mRotationButtonController.getIconResId()); 199 mKeyButtonView.setImageDrawable(mAnimatedDrawable); 200 mKeyButtonView.setColors(lightIconColor, darkIconColor); 201 } 202 203 @Override setOnClickListener(View.OnClickListener onClickListener)204 public void setOnClickListener(View.OnClickListener onClickListener) { 205 mKeyButtonView.setOnClickListener(onClickListener); 206 } 207 208 @Override setOnHoverListener(View.OnHoverListener onHoverListener)209 public void setOnHoverListener(View.OnHoverListener onHoverListener) { 210 mKeyButtonView.setOnHoverListener(onHoverListener); 211 } 212 213 @Override getImageDrawable()214 public Drawable getImageDrawable() { 215 return mAnimatedDrawable; 216 } 217 218 @Override setDarkIntensity(float darkIntensity)219 public void setDarkIntensity(float darkIntensity) { 220 mKeyButtonView.setDarkIntensity(darkIntensity); 221 } 222 223 @Override onTaskbarStateChanged(boolean taskbarVisible, boolean taskbarStashed)224 public void onTaskbarStateChanged(boolean taskbarVisible, boolean taskbarStashed) { 225 mIsTaskbarVisible = taskbarVisible; 226 mIsTaskbarStashed = taskbarStashed; 227 228 if (!mIsShowing) return; 229 230 final Position newPosition = mPositionCalculator 231 .calculatePosition(mDisplayRotation, mIsTaskbarVisible, mIsTaskbarStashed); 232 233 if (newPosition.getTranslationX() != mPosition.getTranslationX() 234 || newPosition.getTranslationY() != mPosition.getTranslationY()) { 235 updateTranslation(newPosition, /* animate */ true); 236 mPosition = newPosition; 237 } 238 } 239 240 /** 241 * Updates resources that could be changed in runtime, should be called on configuration 242 * change with changes diff integer mask 243 * @param configurationChanges - configuration changes with flags from ActivityInfo e.g. 244 * {@link android.content.pm.ActivityInfo#CONFIG_DENSITY} 245 */ onConfigurationChanged(@onfig int configurationChanges)246 public void onConfigurationChanged(@Config int configurationChanges) { 247 if ((configurationChanges & ActivityInfo.CONFIG_DENSITY) != 0 248 || (configurationChanges & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) { 249 updateDimensionResources(); 250 251 if (mIsShowing) { 252 final LayoutParams layoutParams = adjustViewPositionAndCreateLayoutParams(); 253 mWindowManager.updateViewLayout(mKeyButtonContainer, layoutParams); 254 } 255 } 256 257 if ((configurationChanges & ActivityInfo.CONFIG_LOCALE) != 0) { 258 mKeyButtonView.setContentDescription(mContext.getString(mContentDescriptionResource)); 259 } 260 } 261 adjustViewPositionAndCreateLayoutParams()262 private LayoutParams adjustViewPositionAndCreateLayoutParams() { 263 final LayoutParams lp = new LayoutParams( 264 mContainerSize, 265 mContainerSize, 266 /* xpos */ 0, /* ypos */ 0, LayoutParams.TYPE_NAVIGATION_BAR_PANEL, 267 LayoutParams.FLAG_NOT_FOCUSABLE, 268 PixelFormat.TRANSLUCENT); 269 270 lp.privateFlags |= LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 271 lp.setTitle("FloatingRotationButton"); 272 lp.setFitInsetsTypes(/* types */ 0); 273 274 mDisplayRotation = mWindowManager.getDefaultDisplay().getRotation(); 275 mPosition = mPositionCalculator 276 .calculatePosition(mDisplayRotation, mIsTaskbarVisible, mIsTaskbarStashed); 277 278 lp.gravity = mPosition.getGravity(); 279 ((FrameLayout.LayoutParams) mKeyButtonView.getLayoutParams()).gravity = 280 mPosition.getGravity(); 281 282 updateTranslation(mPosition, /* animate */ false); 283 284 return lp; 285 } 286 updateTranslation(Position position, boolean animate)287 private void updateTranslation(Position position, boolean animate) { 288 final int translationX = position.getTranslationX(); 289 final int translationY = position.getTranslationY(); 290 291 if (animate) { 292 mKeyButtonView 293 .animate() 294 .translationX(translationX) 295 .translationY(translationY) 296 .setDuration(MARGIN_ANIMATION_DURATION_MILLIS) 297 .setInterpolator(new AccelerateDecelerateInterpolator()) 298 .withEndAction(() -> { 299 if (mUpdatesCallback != null && mIsShowing) { 300 mUpdatesCallback.onPositionChanged(); 301 } 302 }) 303 .start(); 304 } else { 305 mKeyButtonView.setTranslationX(translationX); 306 mKeyButtonView.setTranslationY(translationY); 307 } 308 } 309 } 310