1 /* 2 * Copyright (C) 2022 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 android.inputmethodservice.navigationbar; 18 19 import static android.inputmethodservice.navigationbar.NavigationBarConstants.DARK_MODE_ICON_COLOR_SINGLE_TONE; 20 import static android.inputmethodservice.navigationbar.NavigationBarConstants.LIGHT_MODE_ICON_COLOR_SINGLE_TONE; 21 import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVBAR_BACK_BUTTON_IME_OFFSET; 22 import static android.inputmethodservice.navigationbar.NavigationBarUtils.dpToPx; 23 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; 24 25 import android.animation.ObjectAnimator; 26 import android.animation.PropertyValuesHolder; 27 import android.annotation.DrawableRes; 28 import android.annotation.FloatRange; 29 import android.app.StatusBarManager; 30 import android.content.Context; 31 import android.content.res.Configuration; 32 import android.graphics.Canvas; 33 import android.util.AttributeSet; 34 import android.util.Log; 35 import android.util.SparseArray; 36 import android.view.Display; 37 import android.view.MotionEvent; 38 import android.view.Surface; 39 import android.view.View; 40 import android.view.animation.Interpolator; 41 import android.view.animation.PathInterpolator; 42 import android.view.inputmethod.InputMethodManager; 43 import android.widget.FrameLayout; 44 45 import java.util.function.Consumer; 46 47 /** 48 * @hide 49 */ 50 public final class NavigationBarView extends FrameLayout { 51 private static final boolean DEBUG = false; 52 private static final String TAG = "NavBarView"; 53 54 // Copied from com.android.systemui.animation.Interpolators#FAST_OUT_SLOW_IN 55 private static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); 56 57 // The current view is always mHorizontal. 58 View mCurrentView = null; 59 private View mHorizontal; 60 61 private int mCurrentRotation = -1; 62 63 int mDisabledFlags = 0; 64 int mNavigationIconHints = StatusBarManager.NAVIGATION_HINT_BACK_ALT; 65 private final int mNavBarMode = NAV_BAR_MODE_GESTURAL; 66 67 private KeyButtonDrawable mBackIcon; 68 private KeyButtonDrawable mImeSwitcherIcon; 69 private Context mLightContext; 70 private final int mLightIconColor; 71 private final int mDarkIconColor; 72 73 private final android.inputmethodservice.navigationbar.DeadZone mDeadZone; 74 private boolean mDeadZoneConsuming = false; 75 76 private final SparseArray<ButtonDispatcher> mButtonDispatchers = new SparseArray<>(); 77 private Configuration mConfiguration; 78 private Configuration mTmpLastConfiguration; 79 80 private NavigationBarInflaterView mNavigationInflaterView; 81 NavigationBarView(Context context, AttributeSet attrs)82 public NavigationBarView(Context context, AttributeSet attrs) { 83 super(context, attrs); 84 85 mLightContext = context; 86 mLightIconColor = LIGHT_MODE_ICON_COLOR_SINGLE_TONE; 87 mDarkIconColor = DARK_MODE_ICON_COLOR_SINGLE_TONE; 88 89 mConfiguration = new Configuration(); 90 mTmpLastConfiguration = new Configuration(); 91 mConfiguration.updateFrom(context.getResources().getConfiguration()); 92 93 mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_back, 94 new ButtonDispatcher(com.android.internal.R.id.input_method_nav_back)); 95 mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_ime_switcher, 96 new ButtonDispatcher(com.android.internal.R.id.input_method_nav_ime_switcher)); 97 mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_home_handle, 98 new ButtonDispatcher(com.android.internal.R.id.input_method_nav_home_handle)); 99 100 mDeadZone = new android.inputmethodservice.navigationbar.DeadZone(this); 101 102 getImeSwitchButton().setOnClickListener(view -> view.getContext() 103 .getSystemService(InputMethodManager.class).showInputMethodPicker()); 104 } 105 106 @Override onInterceptTouchEvent(MotionEvent event)107 public boolean onInterceptTouchEvent(MotionEvent event) { 108 return shouldDeadZoneConsumeTouchEvents(event) || super.onInterceptTouchEvent(event); 109 } 110 111 @Override onTouchEvent(MotionEvent event)112 public boolean onTouchEvent(MotionEvent event) { 113 shouldDeadZoneConsumeTouchEvents(event); 114 return super.onTouchEvent(event); 115 } 116 shouldDeadZoneConsumeTouchEvents(MotionEvent event)117 private boolean shouldDeadZoneConsumeTouchEvents(MotionEvent event) { 118 int action = event.getActionMasked(); 119 if (action == MotionEvent.ACTION_DOWN) { 120 mDeadZoneConsuming = false; 121 } 122 if (mDeadZone.onTouchEvent(event) || mDeadZoneConsuming) { 123 switch (action) { 124 case MotionEvent.ACTION_DOWN: 125 mDeadZoneConsuming = true; 126 break; 127 case MotionEvent.ACTION_CANCEL: 128 case MotionEvent.ACTION_UP: 129 mDeadZoneConsuming = false; 130 break; 131 } 132 return true; 133 } 134 return false; 135 } 136 getCurrentView()137 public View getCurrentView() { 138 return mCurrentView; 139 } 140 141 /** 142 * Applies {@code consumer} to each of the nav bar views. 143 */ forEachView(Consumer<View> consumer)144 public void forEachView(Consumer<View> consumer) { 145 if (mHorizontal != null) { 146 consumer.accept(mHorizontal); 147 } 148 } 149 getBackButton()150 public ButtonDispatcher getBackButton() { 151 return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_back); 152 } 153 getImeSwitchButton()154 public ButtonDispatcher getImeSwitchButton() { 155 return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_ime_switcher); 156 } 157 getHomeHandle()158 public ButtonDispatcher getHomeHandle() { 159 return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_home_handle); 160 } 161 getButtonDispatchers()162 public SparseArray<ButtonDispatcher> getButtonDispatchers() { 163 return mButtonDispatchers; 164 } 165 reloadNavIcons()166 private void reloadNavIcons() { 167 updateIcons(Configuration.EMPTY); 168 } 169 updateIcons(Configuration oldConfig)170 private void updateIcons(Configuration oldConfig) { 171 final boolean orientationChange = oldConfig.orientation != mConfiguration.orientation; 172 final boolean densityChange = oldConfig.densityDpi != mConfiguration.densityDpi; 173 final boolean dirChange = 174 oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirection(); 175 176 if (densityChange || dirChange) { 177 mImeSwitcherIcon = getDrawable(com.android.internal.R.drawable.ic_ime_switcher); 178 } 179 if (orientationChange || densityChange || dirChange) { 180 mBackIcon = getBackDrawable(); 181 } 182 } 183 getBackDrawable()184 private KeyButtonDrawable getBackDrawable() { 185 KeyButtonDrawable drawable = getDrawable(com.android.internal.R.drawable.ic_ime_nav_back); 186 orientBackButton(drawable); 187 return drawable; 188 } 189 190 /** 191 * @return whether this nav bar mode is edge to edge 192 */ isGesturalMode(int mode)193 public static boolean isGesturalMode(int mode) { 194 return mode == NAV_BAR_MODE_GESTURAL; 195 } 196 orientBackButton(KeyButtonDrawable drawable)197 private void orientBackButton(KeyButtonDrawable drawable) { 198 final boolean useAltBack = 199 (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; 200 final boolean isRtl = mConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 201 float degrees = useAltBack ? (isRtl ? 90 : -90) : 0; 202 if (drawable.getRotation() == degrees) { 203 return; 204 } 205 206 if (isGesturalMode(mNavBarMode)) { 207 drawable.setRotation(degrees); 208 return; 209 } 210 211 // Animate the back button's rotation to the new degrees and only in portrait move up the 212 // back button to line up with the other buttons 213 float targetY = useAltBack 214 ? -dpToPx(NAVBAR_BACK_BUTTON_IME_OFFSET, getResources()) 215 : 0; 216 ObjectAnimator navBarAnimator = ObjectAnimator.ofPropertyValuesHolder(drawable, 217 PropertyValuesHolder.ofFloat(KeyButtonDrawable.KEY_DRAWABLE_ROTATE, degrees), 218 PropertyValuesHolder.ofFloat(KeyButtonDrawable.KEY_DRAWABLE_TRANSLATE_Y, targetY)); 219 navBarAnimator.setInterpolator(FAST_OUT_SLOW_IN); 220 navBarAnimator.setDuration(200); 221 navBarAnimator.start(); 222 } 223 getDrawable(@rawableRes int icon)224 private KeyButtonDrawable getDrawable(@DrawableRes int icon) { 225 return KeyButtonDrawable.create(mLightContext, mLightIconColor, mDarkIconColor, icon, 226 true /* hasShadow */, null /* ovalBackgroundColor */); 227 } 228 229 @Override setLayoutDirection(int layoutDirection)230 public void setLayoutDirection(int layoutDirection) { 231 reloadNavIcons(); 232 233 super.setLayoutDirection(layoutDirection); 234 } 235 236 /** 237 * Updates the navigation icons based on {@code hints}. 238 * 239 * @param hints bit flags defined in {@link StatusBarManager}. 240 */ setNavigationIconHints(int hints)241 public void setNavigationIconHints(int hints) { 242 if (hints == mNavigationIconHints) return; 243 final boolean newBackAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; 244 final boolean oldBackAlt = 245 (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; 246 if (newBackAlt != oldBackAlt) { 247 //onImeVisibilityChanged(newBackAlt); 248 } 249 250 if (DEBUG) { 251 android.widget.Toast.makeText(getContext(), "Navigation icon hints = " + hints, 500) 252 .show(); 253 } 254 mNavigationIconHints = hints; 255 updateNavButtonIcons(); 256 } 257 updateNavButtonIcons()258 private void updateNavButtonIcons() { 259 // We have to replace or restore the back and home button icons when exiting or entering 260 // carmode, respectively. Recents are not available in CarMode in nav bar so change 261 // to recent icon is not required. 262 KeyButtonDrawable backIcon = mBackIcon; 263 orientBackButton(backIcon); 264 getBackButton().setImageDrawable(backIcon); 265 266 getImeSwitchButton().setImageDrawable(mImeSwitcherIcon); 267 268 // Update IME button visibility, a11y and rotate button always overrides the appearance 269 final boolean imeSwitcherVisible = 270 (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN) != 0; 271 getImeSwitchButton().setVisibility(imeSwitcherVisible ? View.VISIBLE : View.INVISIBLE); 272 273 getBackButton().setVisibility(View.VISIBLE); 274 getHomeHandle().setVisibility(View.INVISIBLE); 275 276 // We used to be reporting the touch regions via notifyActiveTouchRegions() here. 277 // TODO(b/215593010): Consider taking care of this in the Launcher side. 278 } 279 getContextDisplay()280 private Display getContextDisplay() { 281 return getContext().getDisplay(); 282 } 283 284 @Override onFinishInflate()285 public void onFinishInflate() { 286 super.onFinishInflate(); 287 mNavigationInflaterView = findViewById(com.android.internal.R.id.input_method_nav_inflater); 288 mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers); 289 290 updateOrientationViews(); 291 reloadNavIcons(); 292 } 293 294 @Override onDraw(Canvas canvas)295 protected void onDraw(Canvas canvas) { 296 mDeadZone.onDraw(canvas); 297 super.onDraw(canvas); 298 } 299 updateOrientationViews()300 private void updateOrientationViews() { 301 mHorizontal = findViewById(com.android.internal.R.id.input_method_nav_horizontal); 302 303 updateCurrentView(); 304 } 305 updateCurrentView()306 private void updateCurrentView() { 307 resetViews(); 308 mCurrentView = mHorizontal; 309 mCurrentView.setVisibility(View.VISIBLE); 310 mCurrentRotation = getContextDisplay().getRotation(); 311 mNavigationInflaterView.setAlternativeOrder(mCurrentRotation == Surface.ROTATION_90); 312 mNavigationInflaterView.updateButtonDispatchersCurrentView(); 313 } 314 resetViews()315 private void resetViews() { 316 mHorizontal.setVisibility(View.GONE); 317 } 318 reorient()319 private void reorient() { 320 updateCurrentView(); 321 322 final android.inputmethodservice.navigationbar.NavigationBarFrame frame = 323 getRootView().findViewByPredicate(view -> view instanceof NavigationBarFrame); 324 frame.setDeadZone(mDeadZone); 325 mDeadZone.onConfigurationChanged(mCurrentRotation); 326 327 if (DEBUG) { 328 Log.d(TAG, "reorient(): rot=" + mCurrentRotation); 329 } 330 331 // Resolve layout direction if not resolved since components changing layout direction such 332 // as changing languages will recreate this view and the direction will be resolved later 333 if (!isLayoutDirectionResolved()) { 334 resolveLayoutDirection(); 335 } 336 updateNavButtonIcons(); 337 } 338 339 @Override onConfigurationChanged(Configuration newConfig)340 protected void onConfigurationChanged(Configuration newConfig) { 341 super.onConfigurationChanged(newConfig); 342 mTmpLastConfiguration.updateFrom(mConfiguration); 343 final int changes = mConfiguration.updateFrom(newConfig); 344 345 updateIcons(mTmpLastConfiguration); 346 if (mTmpLastConfiguration.densityDpi != mConfiguration.densityDpi 347 || mTmpLastConfiguration.getLayoutDirection() 348 != mConfiguration.getLayoutDirection()) { 349 // If car mode or density changes, we need to reset the icons. 350 updateNavButtonIcons(); 351 } 352 } 353 354 @Override onAttachedToWindow()355 protected void onAttachedToWindow() { 356 super.onAttachedToWindow(); 357 // This needs to happen first as it can changed the enabled state which can affect whether 358 // the back button is visible 359 requestApplyInsets(); 360 reorient(); 361 updateNavButtonIcons(); 362 } 363 364 @Override onDetachedFromWindow()365 protected void onDetachedFromWindow() { 366 super.onDetachedFromWindow(); 367 for (int i = 0; i < mButtonDispatchers.size(); ++i) { 368 mButtonDispatchers.valueAt(i).onDestroy(); 369 } 370 } 371 372 /** 373 * Updates the dark intensity. 374 * 375 * @param intensity The intensity of darkness from {@code 0.0f} to {@code 1.0f}. 376 */ setDarkIntensity(@loatRangefrom = 0.0f, to = 1.0f) float intensity)377 public void setDarkIntensity(@FloatRange(from = 0.0f, to = 1.0f) float intensity) { 378 for (int i = 0; i < mButtonDispatchers.size(); ++i) { 379 mButtonDispatchers.valueAt(i).setDarkIntensity(intensity); 380 } 381 } 382 } 383