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