/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.taskbar; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IME_SWITCH; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS; import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_KEYGUARD; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.annotation.DrawableRes; import android.annotation.IdRes; import android.annotation.LayoutRes; import android.content.pm.ActivityInfo.Config; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.graphics.Rect; import android.graphics.Region; import android.graphics.Region.Op; import android.graphics.drawable.AnimatedVectorDrawable; import android.util.Property; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnHoverListener; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AlphaUpdateListener; import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton; import com.android.launcher3.util.MultiValueAlpha; import com.android.quickstep.AnimatedFloat; import com.android.systemui.shared.rotation.FloatingRotationButton; import com.android.systemui.shared.rotation.RotationButton; import com.android.systemui.shared.rotation.RotationButtonController; import java.util.ArrayList; import java.util.function.IntPredicate; /** * Controller for managing nav bar buttons in taskbar */ public class NavbarButtonsViewController { private final Rect mTempRect = new Rect(); private static final int FLAG_SWITCHER_SUPPORTED = 1 << 0; private static final int FLAG_IME_VISIBLE = 1 << 1; private static final int FLAG_ROTATION_BUTTON_VISIBLE = 1 << 2; private static final int FLAG_A11Y_VISIBLE = 1 << 3; private static final int FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE = 1 << 4; private static final int FLAG_KEYGUARD_VISIBLE = 1 << 5; private static final int FLAG_KEYGUARD_OCCLUDED = 1 << 6; private static final int FLAG_DISABLE_HOME = 1 << 7; private static final int FLAG_DISABLE_RECENTS = 1 << 8; private static final int FLAG_DISABLE_BACK = 1 << 9; private static final int FLAG_NOTIFICATION_SHADE_EXPANDED = 1 << 10; private static final int FLAG_SCREEN_PINNING_ACTIVE = 1 << 11; private static final int MASK_IME_SWITCHER_VISIBLE = FLAG_SWITCHER_SUPPORTED | FLAG_IME_VISIBLE; private final ArrayList mPropertyHolders = new ArrayList<>(); private final ArrayList mAllButtons = new ArrayList<>(); private int mState; private final TaskbarActivityContext mContext; private final FrameLayout mNavButtonsView; private final ViewGroup mNavButtonContainer; // Used for IME+A11Y buttons private final ViewGroup mEndContextualContainer; private final ViewGroup mStartContextualContainer; private final int mLightIconColor; private final int mDarkIconColor; private final AnimatedFloat mTaskbarNavButtonTranslationY = new AnimatedFloat( this::updateNavButtonTranslationY); private final AnimatedFloat mTaskbarNavButtonTranslationYForIme = new AnimatedFloat( this::updateNavButtonTranslationY); // Only applies to mTaskbarNavButtonTranslationY private final AnimatedFloat mNavButtonTranslationYMultiplier = new AnimatedFloat( this::updateNavButtonTranslationY); private final AnimatedFloat mTaskbarNavButtonDarkIntensity = new AnimatedFloat( this::updateNavButtonDarkIntensity); private final AnimatedFloat mNavButtonDarkIntensityMultiplier = new AnimatedFloat( this::updateNavButtonDarkIntensity); private final RotationButtonListener mRotationButtonListener = new RotationButtonListener(); private final Rect mFloatingRotationButtonBounds = new Rect(); // Initialized in init. private TaskbarControllers mControllers; private View mA11yButton; private int mSysuiStateFlags; private View mBackButton; private FloatingRotationButton mFloatingRotationButton; public NavbarButtonsViewController(TaskbarActivityContext context, FrameLayout navButtonsView) { mContext = context; mNavButtonsView = navButtonsView; mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons); mEndContextualContainer = mNavButtonsView.findViewById(R.id.end_contextual_buttons); mStartContextualContainer = mNavButtonsView.findViewById(R.id.start_contextual_buttons); mLightIconColor = context.getColor(R.color.taskbar_nav_icon_light_color); mDarkIconColor = context.getColor(R.color.taskbar_nav_icon_dark_color); } /** * Initializes the controller */ public void init(TaskbarControllers controllers) { mControllers = controllers; mNavButtonsView.getLayoutParams().height = mContext.getDeviceProfile().taskbarSize; mNavButtonTranslationYMultiplier.value = 1; boolean isThreeButtonNav = mContext.isThreeButtonNav(); // IME switcher View imeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH, isThreeButtonNav ? mStartContextualContainer : mEndContextualContainer, mControllers.navButtonController, R.id.ime_switcher); mPropertyHolders.add(new StatePropertyHolder(imeSwitcherButton, flags -> ((flags & MASK_IME_SWITCHER_VISIBLE) == MASK_IME_SWITCHER_VISIBLE) && ((flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0))); mPropertyHolders.add(new StatePropertyHolder( mControllers.taskbarViewController.getTaskbarIconAlpha() .getProperty(ALPHA_INDEX_KEYGUARD), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && (flags & FLAG_SCREEN_PINNING_ACTIVE) == 0, MultiValueAlpha.VALUE, 1, 0)); mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0, AnimatedFloat.VALUE, 1, 0)); // Force nav buttons (specifically back button) to be visible during setup wizard. boolean isInSetup = !mContext.isUserSetupComplete(); boolean alwaysShowButtons = isThreeButtonNav || isInSetup; // Make sure to remove nav bar buttons translation when notification shade is expanded or // IME is showing (add separate translation for IME). int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE; mPropertyHolders.add(new StatePropertyHolder(mNavButtonTranslationYMultiplier, flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE, 0, 1)); // Center nav buttons in new height for IME. float transForIme = (mContext.getDeviceProfile().taskbarSize - mContext.getTaskbarHeightForIme()) / 2f; // For gesture nav, nav buttons only show for IME anyway so keep them translated down. float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme; mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme, flags -> (flags & FLAG_IME_VISIBLE) != 0, AnimatedFloat.VALUE, transForIme, defaultButtonTransY)); if (alwaysShowButtons) { initButtons(mNavButtonContainer, mEndContextualContainer, mControllers.navButtonController); if (isInSetup) { // Since setup wizard only has back button enabled, it looks strange to be // end-aligned, so start-align instead. FrameLayout.LayoutParams navButtonsLayoutParams = (FrameLayout.LayoutParams) mNavButtonContainer.getLayoutParams(); navButtonsLayoutParams.setMarginStart(navButtonsLayoutParams.getMarginEnd()); navButtonsLayoutParams.setMarginEnd(0); navButtonsLayoutParams.gravity = Gravity.START; mNavButtonContainer.requestLayout(); // TODO(b/210906568) Dark intensity is currently not propagated during setup, so set // it based on dark theme for now. int mode = mContext.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; boolean isDarkTheme = mode == Configuration.UI_MODE_NIGHT_YES; mTaskbarNavButtonDarkIntensity.updateValue(isDarkTheme ? 0 : 1); } // Animate taskbar background when any of these flags are enabled int flagsToShowBg = FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE | FLAG_NOTIFICATION_SHADE_EXPANDED; mPropertyHolders.add(new StatePropertyHolder( mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(), flags -> (flags & flagsToShowBg) != 0, AnimatedFloat.VALUE, 1, 0)); // Rotation button RotationButton rotationButton = new RotationButtonImpl( addButton(mEndContextualContainer, R.id.rotate_suggestion, R.layout.taskbar_contextual_button)); rotationButton.hide(); mControllers.rotationButtonController.setRotationButton(rotationButton, null); } else { mFloatingRotationButton = new FloatingRotationButton(mContext, R.string.accessibility_rotate_button, R.layout.rotate_suggestion, R.id.rotate_suggestion, R.dimen.floating_rotation_button_min_margin, R.dimen.rounded_corner_content_padding, R.dimen.floating_rotation_button_taskbar_left_margin, R.dimen.floating_rotation_button_taskbar_bottom_margin, R.dimen.floating_rotation_button_diameter, R.dimen.key_button_ripple_max_width); mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton, mRotationButtonListener); View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK, mStartContextualContainer, mControllers.navButtonController, R.id.back); imeDownButton.setRotation(Utilities.isRtl(mContext.getResources()) ? 90 : -90); // Rotate when Ime visible mPropertyHolders.add(new StatePropertyHolder(imeDownButton, flags -> (flags & FLAG_IME_VISIBLE) != 0)); } applyState(); mPropertyHolders.forEach(StatePropertyHolder::endAnimation); } private void initButtons(ViewGroup navContainer, ViewGroup endContainer, TaskbarNavButtonController navButtonController) { mBackButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK, mNavButtonContainer, mControllers.navButtonController, R.id.back); mPropertyHolders.add(new StatePropertyHolder(mBackButton, flags -> { // Show only if not disabled, and if not on the keyguard or otherwise only when // the bouncer or a lockscreen app is showing above the keyguard boolean showingOnKeyguard = (flags & FLAG_KEYGUARD_VISIBLE) == 0 || (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0 || (flags & FLAG_KEYGUARD_OCCLUDED) != 0; return (flags & FLAG_DISABLE_BACK) == 0 && ((flags & FLAG_KEYGUARD_VISIBLE) == 0 || showingOnKeyguard); })); boolean isRtl = Utilities.isRtl(mContext.getResources()); mPropertyHolders.add(new StatePropertyHolder( mBackButton, flags -> (flags & FLAG_IME_VISIBLE) != 0, View.ROTATION, isRtl ? 90 : -90, 0)); // Translate back button to be at end/start of other buttons for keyguard int navButtonSize = mContext.getResources().getDimensionPixelSize( R.dimen.taskbar_nav_buttons_size); mPropertyHolders.add(new StatePropertyHolder( mBackButton, flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0 || (flags & FLAG_KEYGUARD_VISIBLE) != 0, VIEW_TRANSLATE_X, navButtonSize * (isRtl ? -2 : 2), 0)); // home and recents buttons View homeButton = addButton(R.drawable.ic_sysbar_home, BUTTON_HOME, navContainer, navButtonController, R.id.home); mPropertyHolders.add(new StatePropertyHolder(homeButton, flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && (flags & FLAG_DISABLE_HOME) == 0)); View recentsButton = addButton(R.drawable.ic_sysbar_recent, BUTTON_RECENTS, navContainer, navButtonController, R.id.recent_apps); mPropertyHolders.add(new StatePropertyHolder(recentsButton, flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && (flags & FLAG_DISABLE_RECENTS) == 0)); // A11y button mA11yButton = addButton(R.drawable.ic_sysbar_accessibility_button, BUTTON_A11Y, endContainer, navButtonController, R.id.accessibility_button, R.layout.taskbar_contextual_button); mPropertyHolders.add(new StatePropertyHolder(mA11yButton, flags -> (flags & FLAG_A11Y_VISIBLE) != 0 && (flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0)); } private void parseSystemUiFlags(int sysUiStateFlags) { mSysuiStateFlags = sysUiStateFlags; boolean isImeVisible = (sysUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0; boolean isImeSwitcherShowing = (sysUiStateFlags & SYSUI_STATE_IME_SWITCHER_SHOWING) != 0; boolean a11yVisible = (sysUiStateFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0; boolean isHomeDisabled = (sysUiStateFlags & SYSUI_STATE_HOME_DISABLED) != 0; boolean isRecentsDisabled = (sysUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0; boolean isBackDisabled = (sysUiStateFlags & SYSUI_STATE_BACK_DISABLED) != 0; int shadeExpandedFlags = SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; boolean isNotificationShadeExpanded = (sysUiStateFlags & shadeExpandedFlags) != 0; boolean isScreenPinningActive = (sysUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0; // TODO(b/202218289) we're getting IME as not visible on lockscreen from system updateStateForFlag(FLAG_IME_VISIBLE, isImeVisible); updateStateForFlag(FLAG_SWITCHER_SUPPORTED, isImeSwitcherShowing); updateStateForFlag(FLAG_A11Y_VISIBLE, a11yVisible); updateStateForFlag(FLAG_DISABLE_HOME, isHomeDisabled); updateStateForFlag(FLAG_DISABLE_RECENTS, isRecentsDisabled); updateStateForFlag(FLAG_DISABLE_BACK, isBackDisabled); updateStateForFlag(FLAG_NOTIFICATION_SHADE_EXPANDED, isNotificationShadeExpanded); updateStateForFlag(FLAG_SCREEN_PINNING_ACTIVE, isScreenPinningActive); if (mA11yButton != null) { // Only used in 3 button boolean a11yLongClickable = (sysUiStateFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0; mA11yButton.setLongClickable(a11yLongClickable); } } public void updateStateForSysuiFlags(int systemUiStateFlags, boolean skipAnim) { if (systemUiStateFlags == mSysuiStateFlags) { return; } parseSystemUiFlags(systemUiStateFlags); applyState(); if (skipAnim) { mPropertyHolders.forEach(StatePropertyHolder::endAnimation); } } /** * Should be called when we need to show back button for bouncer */ public void setBackForBouncer(boolean isBouncerVisible) { updateStateForFlag(FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE, isBouncerVisible); applyState(); } /** * Slightly misnamed, but should be called when keyguard OR AOD is showing. * We consider keyguardVisible when it's showing bouncer OR is occlucded by another app */ public void setKeyguardVisible(boolean isKeyguardVisible, boolean isKeyguardOccluded) { updateStateForFlag(FLAG_KEYGUARD_VISIBLE, isKeyguardVisible || isKeyguardOccluded); updateStateForFlag(FLAG_KEYGUARD_OCCLUDED, isKeyguardOccluded); applyState(); } /** * Returns true if IME bar is visible */ public boolean isImeVisible() { return (mState & FLAG_IME_VISIBLE) != 0; } /** * Returns true if the home button is disabled */ public boolean isHomeDisabled() { return (mState & FLAG_DISABLE_HOME) != 0; } /** * Returns true if the recents (overview) button is disabled */ public boolean isRecentsDisabled() { return (mState & FLAG_DISABLE_RECENTS) != 0; } /** * Adds the bounds corresponding to all visible buttons to provided region */ public void addVisibleButtonsRegion(TaskbarDragLayer parent, Region outRegion) { int count = mAllButtons.size(); for (int i = 0; i < count; i++) { View button = mAllButtons.get(i); if (button.getVisibility() == View.VISIBLE) { parent.getDescendantRectRelativeToSelf(button, mTempRect); outRegion.op(mTempRect, Op.UNION); } } } /** Use to set the translationY for the all nav+contextual buttons */ public AnimatedFloat getTaskbarNavButtonTranslationY() { return mTaskbarNavButtonTranslationY; } /** Use to set the dark intensity for the all nav+contextual buttons */ public AnimatedFloat getTaskbarNavButtonDarkIntensity() { return mTaskbarNavButtonDarkIntensity; } /** Use to determine whether to use the dark intensity requested by the underlying app */ public AnimatedFloat getNavButtonDarkIntensityMultiplier() { return mNavButtonDarkIntensityMultiplier; } /** * Does not call {@link #applyState()}. Don't forget to! */ private void updateStateForFlag(int flag, boolean enabled) { if (enabled) { mState |= flag; } else { mState &= ~flag; } } private void applyState() { int count = mPropertyHolders.size(); for (int i = 0; i < count; i++) { mPropertyHolders.get(i).setState(mState); } } private void updateNavButtonTranslationY() { float normalTranslationY = mTaskbarNavButtonTranslationY.value * mNavButtonTranslationYMultiplier.value; float otherTranslationY = mTaskbarNavButtonTranslationYForIme.value; mNavButtonsView.setTranslationY(normalTranslationY + otherTranslationY); } private void updateNavButtonDarkIntensity() { float darkIntensity = mTaskbarNavButtonDarkIntensity.value * mNavButtonDarkIntensityMultiplier.value; int iconColor = (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, mLightIconColor, mDarkIconColor); for (ImageView button : mAllButtons) { button.setImageTintList(ColorStateList.valueOf(iconColor)); } } private ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType, ViewGroup parent, TaskbarNavButtonController navButtonController, @IdRes int id) { return addButton(drawableId, buttonType, parent, navButtonController, id, R.layout.taskbar_nav_button); } private ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType, ViewGroup parent, TaskbarNavButtonController navButtonController, @IdRes int id, @LayoutRes int layoutId) { ImageView buttonView = addButton(parent, id, layoutId); buttonView.setImageResource(drawableId); buttonView.setOnClickListener(view -> navButtonController.onButtonClick(buttonType)); buttonView.setOnLongClickListener(view -> navButtonController.onButtonLongClick(buttonType)); return buttonView; } private ImageView addButton(ViewGroup parent, @IdRes int id, @LayoutRes int layoutId) { ImageView buttonView = (ImageView) mContext.getLayoutInflater() .inflate(layoutId, parent, false); buttonView.setId(id); parent.addView(buttonView); mAllButtons.add(buttonView); return buttonView; } public boolean isEventOverAnyItem(MotionEvent ev) { return mFloatingRotationButtonBounds.contains((int) ev.getX(), (int) ev.getY()); } public void onConfigurationChanged(@Config int configChanges) { if (mFloatingRotationButton != null) { mFloatingRotationButton.onConfigurationChanged(configChanges); } } public void onDestroy() { mPropertyHolders.clear(); mControllers.rotationButtonController.unregisterListeners(); if (mFloatingRotationButton != null) { mFloatingRotationButton.hide(); } } private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback { @Override public void onVisibilityChanged(boolean isVisible) { if (isVisible) { mFloatingRotationButton.getCurrentView() .getBoundsOnScreen(mFloatingRotationButtonBounds); } else { mFloatingRotationButtonBounds.setEmpty(); } } } private class RotationButtonImpl implements RotationButton { private final ImageView mButton; private AnimatedVectorDrawable mImageDrawable; RotationButtonImpl(ImageView button) { mButton = button; } @Override public void setRotationButtonController(RotationButtonController rotationButtonController) { // TODO(b/187754252) UI polish, different icons based on light/dark context, etc mImageDrawable = (AnimatedVectorDrawable) mButton.getContext() .getDrawable(rotationButtonController.getIconResId()); mButton.setImageDrawable(mImageDrawable); mButton.setContentDescription(mButton.getResources() .getString(R.string.accessibility_rotate_button)); mImageDrawable.setCallback(mButton); } @Override public View getCurrentView() { return mButton; } @Override public boolean show() { mButton.setVisibility(View.VISIBLE); mState |= FLAG_ROTATION_BUTTON_VISIBLE; applyState(); return true; } @Override public boolean hide() { mButton.setVisibility(View.GONE); mState &= ~FLAG_ROTATION_BUTTON_VISIBLE; applyState(); return true; } @Override public boolean isVisible() { return mButton.getVisibility() == View.VISIBLE; } @Override public void updateIcon(int lightIconColor, int darkIconColor) { // TODO(b/187754252): UI Polish } @Override public void setOnClickListener(OnClickListener onClickListener) { mButton.setOnClickListener(onClickListener); } @Override public void setOnHoverListener(OnHoverListener onHoverListener) { mButton.setOnHoverListener(onHoverListener); } @Override public AnimatedVectorDrawable getImageDrawable() { return mImageDrawable; } @Override public void setDarkIntensity(float darkIntensity) { // TODO(b/187754252) UI polish } @Override public boolean acceptRotationProposal() { return mButton.isAttachedToWindow(); } } private static class StatePropertyHolder { private final float mEnabledValue, mDisabledValue; private final ObjectAnimator mAnimator; private final IntPredicate mEnableCondition; private boolean mIsEnabled = true; StatePropertyHolder(View view, IntPredicate enableCondition) { this(view, enableCondition, LauncherAnimUtils.VIEW_ALPHA, 1, 0); mAnimator.addListener(new AlphaUpdateListener(view)); } StatePropertyHolder(T target, IntPredicate enabledCondition, Property property, float enabledValue, float disabledValue) { mEnableCondition = enabledCondition; mEnabledValue = enabledValue; mDisabledValue = disabledValue; mAnimator = ObjectAnimator.ofFloat(target, property, enabledValue, disabledValue); } public void setState(int flags) { boolean isEnabled = mEnableCondition.test(flags); if (mIsEnabled != isEnabled) { mIsEnabled = isEnabled; mAnimator.cancel(); mAnimator.setFloatValues(mIsEnabled ? mEnabledValue : mDisabledValue); mAnimator.start(); } } public void endAnimation() { if (mAnimator.isRunning()) { mAnimator.end(); } } } }