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