// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.toolbar.optional_button;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Handler;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.Slide;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.view.ViewCompat;
import androidx.core.widget.ImageViewCompat;
import com.google.android.material.color.MaterialColors;
import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.chrome.browser.toolbar.ButtonData;
import org.chromium.chrome.browser.toolbar.ButtonData.ButtonSpec;
import org.chromium.chrome.browser.toolbar.R;
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarButtonVariant;
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarFeatures;
import org.chromium.chrome.browser.toolbar.optional_button.OptionalButtonConstants.TransitionType;
import org.chromium.ui.listmenu.ListMenuButton;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.function.BooleanSupplier;
/** Toolbar button that performs animated transitions between icons. */
class OptionalButtonView extends FrameLayout implements TransitionListener {
private static final int SWAP_TRANSITION_DURATION_MS = 300;
private static final int HIDE_TRANSITION_DURATION_MS = 225;
private final int mCollapsedStateWidthPx;
private final int mExpandedStatePaddingPx;
private TextView mActionChipLabel;
private ImageView mBackground;
private ListMenuButton mButton;
private ImageView mAnimationImage;
private Drawable mIconDrawable;
private ViewGroup mTransitionRoot;
private String mContentDescription;
private String mActionChipLabelString;
private boolean mCurrentButtonSupportsTinting;
private ColorStateList mForegroundColorTint;
private int mBackgroundColorFilter;
private Runnable mOnBeforeHideTransitionCallback;
private Callback<Transition> mFakeBeginTransitionForTesting;
private Handler mHandler;
private Handler mHandlerForTesting;
private @State int mState;
private @AdaptiveToolbarButtonVariant int mCurrentButtonVariant =
AdaptiveToolbarButtonVariant.NONE;
private boolean mCanCurrentButtonShow;
private @ButtonType int mCurrentButtonType;
private @ButtonType int mNextButtonType;
private OnClickListener mClickListener;
private OnLongClickListener mLongClickListener;
private Callback<Integer> mTransitionStartedCallback;
private Callback<Integer> mTransitionFinishedCallback;
private BooleanSupplier mIsAnimationAllowedPredicate;
private final Runnable mCollapseActionChipRunnable =
new Runnable() {
@Override
public void run() {
if (mIsAnimationAllowedPredicate.getAsBoolean()) {
animateActionChipCollapse();
} else {
showIcon(false);
}
}
};
@IntDef({
State.HIDDEN,
State.SHOWING_ICON,
State.SHOWING_ACTION_CHIP,
State.RUNNING_SHOW_TRANSITION,
State.RUNNING_HIDE_TRANSITION,
State.RUNNING_ACTION_CHIP_EXPANSION_TRANSITION,
State.RUNNING_ACTION_CHIP_COLLAPSE_TRANSITION,
State.RUNNING_SWAP_TRANSITION
})
@Retention(RetentionPolicy.SOURCE)
private @interface State {
int HIDDEN = 0;
int SHOWING_ICON = 1;
int SHOWING_ACTION_CHIP = 2;
int RUNNING_SHOW_TRANSITION = 3;
int RUNNING_HIDE_TRANSITION = 4;
int RUNNING_ACTION_CHIP_EXPANSION_TRANSITION = 5;
int RUNNING_ACTION_CHIP_COLLAPSE_TRANSITION = 6;
int RUNNING_SWAP_TRANSITION = 7;
}
@IntDef({ButtonType.STATIC, ButtonType.DYNAMIC})
@Retention(RetentionPolicy.SOURCE)
private @interface ButtonType {
int STATIC = 0;
int DYNAMIC = 1;
}
void setTransitionStartedCallback(Callback<Integer> callback) {
mTransitionStartedCallback = callback;
}
void setTransitionFinishedCallback(Callback<Integer> callback) {
mTransitionFinishedCallback = callback;
}
void setIsAnimationAllowedPredicate(BooleanSupplier isAnimationAllowedPredicate) {
mIsAnimationAllowedPredicate = isAnimationAllowedPredicate;
}
void setOnBeforeHideTransitionCallback(Runnable callback) {
mOnBeforeHideTransitionCallback = callback;
}
void setPaddingStart(int paddingStart) {
setPaddingRelative(paddingStart, getPaddingTop(), getPaddingEnd(), getPaddingBottom());
}
public void cancelTransition() {
if (isRunningTransition()) {
TransitionManager.endTransitions(mTransitionRoot);
}
}
/**
* Updates the button's icon, click handler, description and other attributes with a transition
* animation. The animation that runs depends on the current state of this view (Whether is
* hidden or showing another icon) and the attributes of the new icon (Whether it contains an
* action chip description).
* @param buttonData object containing the new button's icon, handlers, description and other
* attributes. If null then this view starts a hide transition.
*/
void updateButtonWithAnimation(@Nullable ButtonData buttonData) {
// If we receive the same button with the same visibility then there's no need to update.
if (buttonData != null
&& mCurrentButtonVariant == buttonData.getButtonSpec().getButtonVariant()
&& mCanCurrentButtonShow == buttonData.canShow()
&& mIconDrawable == buttonData.getButtonSpec().getDrawable()) {
return;
}
if (mTransitionRoot == null || mIsAnimationAllowedPredicate == null) {
throw new IllegalStateException(
"Both transitionRoot and animationAllowedPredicate must be set before starting "
+ "a transition");
}
boolean isAnimationAllowedByParent = mIsAnimationAllowedPredicate.getAsBoolean();
if (isRunningTransition()) {
// If we are running any transitions then finish them immediately and jump to the next
// state.
TransitionManager.endTransitions(mTransitionRoot);
// TransitionManager.endTransitions calls onTransitionEnd on its own, but it's done
// asynchronously which causes flaky tests. We call it here to ensure the state updates
// and callbacks are executed synchronously.
onTransitionEnd(null);
}
if (mState == State.SHOWING_ACTION_CHIP) {
// If the action chip is expanded then deschedule the collapse task and collapse
// immediately.
getHandler().removeCallbacks(mCollapseActionChipRunnable);
showIcon(false);
mState = getNextState();
}
if (buttonData == null || !buttonData.canShow()) {
mCurrentButtonVariant = AdaptiveToolbarButtonVariant.NONE;
mCanCurrentButtonShow = false;
hide(isAnimationAllowedByParent);
return;
}
ButtonSpec buttonSpec = buttonData.getButtonSpec();
boolean isButtonVariantChanging = mCurrentButtonVariant != buttonSpec.getButtonVariant();
// This boolean is final because it's passed to an inner class (OnGlobalLayoutListener).
final boolean canAnimate = isAnimationAllowedByParent && isButtonVariantChanging;
mCurrentButtonVariant = buttonSpec.getButtonVariant();
mCanCurrentButtonShow = buttonData.canShow();
mCurrentButtonSupportsTinting = buttonSpec.getSupportsTinting();
mIconDrawable = buttonSpec.getDrawable();
mNextButtonType = buttonSpec.isDynamicAction() ? ButtonType.DYNAMIC : ButtonType.STATIC;
if (buttonSpec.getActionChipLabelResId() == Resources.ID_NULL) {
mActionChipLabelString = null;
} else {
mActionChipLabelString =
getContext().getResources().getString(buttonSpec.getActionChipLabelResId());
}
mClickListener = buttonSpec.getOnClickListener();
mLongClickListener = buttonSpec.getOnLongClickListener();
mButton.setEnabled(buttonData.isEnabled());
// Set circular hover highlight for optional button when button variant is profile, share,
// voice search and new tab. Set box hover highlight for the rest of button variants.
if (buttonData.getButtonSpec().getShouldShowHoverHighlight()) {
mButton.setBackgroundResource(R.drawable.toolbar_button_ripple);
} else {
TypedValue themeRes = new TypedValue();
getContext()
.getTheme()
.resolveAttribute(R.attr.selectableItemBackground, themeRes, true);
mButton.setBackgroundResource(themeRes.resourceId);
}
// Set hover state tooltip text for optional toolbar buttons(e.g. share, voice search, new
// tab and profile).
if (buttonSpec.getHoverTooltipTextId() != ButtonSpec.INVALID_TOOLTIP_TEXT_ID
&& mButton != null
&& VERSION.SDK_INT >= Build.VERSION_CODES.O) {
TooltipCompat.setTooltipText(
mButton, getContext().getString(buttonSpec.getHoverTooltipTextId()));
} else {
TooltipCompat.setTooltipText(mButton, null);
}
mContentDescription = buttonSpec.getContentDescription();
// If the transition root hasn't been laid out then try again after the next layout. This
// may happen if the view gets initialized while the activity is not visible (e.g. when a
// setting change forces an activity reset).
if (!ViewCompat.isLaidOut(mTransitionRoot)) {
getViewTreeObserver()
.addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (ViewCompat.isLaidOut(mTransitionRoot)) {
startTransitionToNewButton(canAnimate);
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
} else {
startTransitionToNewButton(canAnimate);
}
}
private void startTransitionToNewButton(boolean canAnimate) {
if (mState == State.HIDDEN && mActionChipLabelString == null) {
showIcon(canAnimate);
} else if (canAnimate && mActionChipLabelString != null) {
animateActionChipExpansion();
} else if (canAnimate && mActionChipLabelString == null) {
animateSwapToNewIcon();
} else {
showIcon(false);
}
}
/**
* Set a view to use as a root for all transition animations. It's used to animate sibling views
* when this one changes width.
* @param transitionRoot
*/
// TODO(salg): Consider getting rid of this property as it can be awkward to have a view
// initiating an animation on its siblings.
void setTransitionRoot(ViewGroup transitionRoot) {
mTransitionRoot = transitionRoot;
}
void setBackgroundColorFilter(int color) {
mBackgroundColorFilter = color;
mBackground.setColorFilter(color);
}
void setBackgroundAlpha(int alpha) {
mBackground.setImageAlpha(alpha);
}
View getBackgroundView() {
return mBackground;
}
void setColorStateList(ColorStateList colorStateList) {
mForegroundColorTint = colorStateList;
if (mCurrentButtonSupportsTinting) {
ImageViewCompat.setImageTintList(mButton, colorStateList);
}
if (colorStateList != null) {
mActionChipLabel.setTextColor(colorStateList);
}
}
void setHandlerForTesting(Handler handler) {
mHandlerForTesting = handler;
}
View getButtonView() {
return mButton;
}
ImageView getAnimationViewForTesting() {
return mAnimationImage;
}
/**
* Constructor for inflating from XML.
* @param context
* @param attrs
*/
public OptionalButtonView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mState = State.HIDDEN;
// TODO(salg): Move these dimensions to an XML file.
float density = getResources().getDisplayMetrics().density;
mCollapsedStateWidthPx = (int) (52 * density);
mExpandedStatePaddingPx = (int) (8 * density);
}
/**
* Gets a handler used to schedule the action chip collapse animation after the action chip
* finishes expanding. Tests can set their own handler with {@code setHandlerForTesting}.
*/
@Override
public Handler getHandler() {
if (mHandlerForTesting != null) {
return mHandlerForTesting;
}
if (mHandler == null) {
mHandler = new Handler(ThreadUtils.getUiThreadLooper());
}
return mHandler;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mBackground = findViewById(R.id.swappable_icon_secondary_background);
mButton = findViewById(R.id.optional_toolbar_button);
mAnimationImage = findViewById(R.id.swappable_icon_animation_image);
mActionChipLabel = findViewById(R.id.action_chip_label);
mBackground.setImageDrawable(
AppCompatResources.getDrawable(
getContext(),
R.drawable.modern_toolbar_text_box_background_with_primary_color));
}
/**
* Listens to all transition starts. This is called even when animations are disabled.
* Implementation of {@link TransitionListener}.
* @param transition Transition that started, not used.
*/
@Override
public void onTransitionStart(Transition transition) {
if (mState != State.RUNNING_ACTION_CHIP_COLLAPSE_TRANSITION) {
// Disable click listeners during the transitions (except action chip collapse, which
// goes to the same icon/action).
mButton.setOnClickListener(null);
mButton.setOnLongClickListener(null);
mButton.setContentDescription(null);
}
if (mTransitionStartedCallback != null) {
mTransitionStartedCallback.onResult(getCurrentTransitionType());
}
}
/**
* Listens to all transition ends. This is called even if the transition is cancelled or if all
* animations are disabled. Implementation of {@link TransitionListener}.
* @param transition Transition that ended, not used.
*/
@Override
public void onTransitionEnd(Transition transition) {
if (mTransitionFinishedCallback != null
&& getCurrentTransitionType() != TransitionType.NONE) {
mTransitionFinishedCallback.onResult(getCurrentTransitionType());
}
mState = getNextState();
mCurrentButtonType = mNextButtonType;
// This image is only used during transitions, it should not be visible afterwards.
mAnimationImage.setVisibility(GONE);
if (mState == State.HIDDEN) {
this.setVisibility(GONE);
} else {
mButton.setVisibility(VISIBLE);
mButton.setImageDrawable(mIconDrawable);
ImageViewCompat.setImageTintList(
mButton, mCurrentButtonSupportsTinting ? mForegroundColorTint : null);
mButton.setOnClickListener(mClickListener);
mButton.setLongClickable(mLongClickListener != null);
mButton.setOnLongClickListener(mLongClickListener);
mButton.setContentDescription(mContentDescription);
}
// When finished expanding the action chip schedule the collapse transition in 3 seconds.
if (mState == State.SHOWING_ACTION_CHIP) {
getHandler()
.postDelayed(
mCollapseActionChipRunnable,
AdaptiveToolbarFeatures.getContextualPageActionDelayMs(
mCurrentButtonVariant));
}
}
/** Implementation of {@link TransitionListener}. Not used. */
@Override
public void onTransitionCancel(Transition transition) {}
/** Implementation of {@link TransitionListener}. Not used. */
@Override
public void onTransitionPause(Transition transition) {}
/** Implementation of {@link TransitionListener}. Not used. */
@Override
public void onTransitionResume(Transition transition) {}
private Transition createSwapIconTransition() {
TransitionSet transition = new TransitionSet();
transition.setOrdering(TransitionSet.ORDERING_TOGETHER);
// All appearing/disappearing views will fade in/out.
Fade fade = new Fade();
// When appearing/disappearing mButton will shrink/grow.
ShrinkTransition shrink = new ShrinkTransition();
shrink.addTarget(mButton);
// When appearing/disappearing mAnimationImage will move from/to the top.
Slide slide = new Slide(Gravity.TOP);
slide.addTarget(mAnimationImage);
transition.addTransition(slide).addTransition(shrink).addTransition(fade);
transition.setDuration(SWAP_TRANSITION_DURATION_MS);
transition.addListener(this);
return transition;
}
private Transition createShowHideTransition() {
TransitionSet transition = new TransitionSet();
transition.setOrdering(TransitionSet.ORDERING_TOGETHER);
Fade fade = new Fade();
// When showing/hiding this view we change its width from/to 0dp, this transition animates
// that width change.
ChangeBounds changeBounds = new ChangeBounds();
// When mButton shows/hides we use a grow/shrink animation.
ShrinkTransition shrink = new ShrinkTransition();
// When mButton and mBackground show up or hide they slide from/to the end (right in LTR,
// left in RTL).
Slide slide = new Slide(Gravity.END);
slide.addTarget(mButton);
slide.addTarget(mBackground);
transition
.addTransition(slide)
.addTransition(shrink)
.addTransition(fade)
.addTransition(changeBounds);
transition.setDuration(HIDE_TRANSITION_DURATION_MS);
transition.addListener(this);
return transition;
}
private Transition createActionChipTransition() {
TransitionSet transitionSet = new TransitionSet();
transitionSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
// During the action chip transition we change this view's width to fit the action chip
// label, this transition animates that change.
ChangeBounds changeBounds = new ChangeBounds();
// The action chip label and the new icon fade in and grow when showing up.
Fade fade = new Fade();
ShrinkTransition shrinkTransition = new ShrinkTransition();
transitionSet
.addTransition(changeBounds)
.addTransition(fade)
.addTransition(shrinkTransition);
transitionSet.setDuration(SWAP_TRANSITION_DURATION_MS);
transitionSet.addListener(this);
return transitionSet;
}
private @TransitionType int getCurrentTransitionType() {
switch (mState) {
case State.RUNNING_ACTION_CHIP_COLLAPSE_TRANSITION:
return TransitionType.COLLAPSING_ACTION_CHIP;
case State.RUNNING_ACTION_CHIP_EXPANSION_TRANSITION:
return TransitionType.EXPANDING_ACTION_CHIP;
case State.RUNNING_HIDE_TRANSITION:
return TransitionType.HIDING;
case State.RUNNING_SHOW_TRANSITION:
return TransitionType.SHOWING;
case State.RUNNING_SWAP_TRANSITION:
return TransitionType.SWAPPING;
case State.HIDDEN:
case State.SHOWING_ACTION_CHIP:
case State.SHOWING_ICON:
return TransitionType.NONE;
default:
throw new IllegalStateException("Unexpected value: " + mState);
}
}
private void setWidth(int widthPx) {
ViewGroup.LayoutParams layoutParams = this.getLayoutParams();
layoutParams.width = widthPx;
setLayoutParams(layoutParams);
}
private boolean isRunningTransition() {
return mState == State.RUNNING_SHOW_TRANSITION
|| mState == State.RUNNING_HIDE_TRANSITION
|| mState == State.RUNNING_ACTION_CHIP_EXPANSION_TRANSITION
|| mState == State.RUNNING_ACTION_CHIP_COLLAPSE_TRANSITION
|| mState == State.RUNNING_SWAP_TRANSITION;
}
private @State int getNextState() {
switch (mState) {
case State.RUNNING_ACTION_CHIP_COLLAPSE_TRANSITION:
case State.RUNNING_SWAP_TRANSITION:
case State.RUNNING_SHOW_TRANSITION:
return State.SHOWING_ICON;
case State.RUNNING_ACTION_CHIP_EXPANSION_TRANSITION:
return State.SHOWING_ACTION_CHIP;
case State.RUNNING_HIDE_TRANSITION:
return State.HIDDEN;
default:
return mState;
}
}
private void animateSwapToNewIcon() {
if (mState != State.SHOWING_ICON) return;
boolean isRevertingToStatic =
mCurrentButtonType == ButtonType.DYNAMIC && mNextButtonType == ButtonType.STATIC;
// Set the background color filter before the transition, these changes are done instantly.
if (mNextButtonType == ButtonType.DYNAMIC) {
mBackground.setColorFilter(mBackgroundColorFilter);
}
// In mSwapIconTransition mAnimationImage always slides from/to the top, and mButton always
// grows/shrinks.
ImageView slidingIcon = mAnimationImage;
ImageView shrinkingIcon = mButton;
Drawable newIcon = mIconDrawable;
Drawable oldIcon = mButton.getDrawable();
ColorStateList oldIconTint = ImageViewCompat.getImageTintList(mButton);
ColorStateList newIconTint = mCurrentButtonSupportsTinting ? mForegroundColorTint : null;
// Prepare icons for the transition, these changes are done instantly.
if (!isRevertingToStatic) {
// In the default transition we want the new icon to slide from the top and the old one
// to shrink.
slidingIcon.setImageDrawable(newIcon);
ImageViewCompat.setImageTintList(slidingIcon, newIconTint);
} else {
// In the reverse transition we want the new icon to grow and the old icon to slide to
// the top
slidingIcon.setImageDrawable(oldIcon);
ImageViewCompat.setImageTintList(slidingIcon, oldIconTint);
slidingIcon.setVisibility(VISIBLE);
shrinkingIcon.setImageDrawable(newIcon);
ImageViewCompat.setImageTintList(shrinkingIcon, newIconTint);
shrinkingIcon.setVisibility(GONE);
}
// Begin a transition, all layout changes after this call will be animated. The animation
// starts at the next frame.
beginDelayedTransition(createSwapIconTransition());
// Default transition.
if (!isRevertingToStatic) {
// New icon slides from the top.
slidingIcon.setVisibility(VISIBLE);
// Old icon shrinks.
shrinkingIcon.setVisibility(GONE);
}
// Reverse transition.
else {
// Old icon slides to the top.
slidingIcon.setVisibility(GONE);
// New icon embiggens.
shrinkingIcon.setVisibility(VISIBLE);
}
// Background shows/hides with a fade animation.
mBackground.setVisibility(mNextButtonType == ButtonType.DYNAMIC ? VISIBLE : GONE);
mState = State.RUNNING_SWAP_TRANSITION;
}
private void animateActionChipExpansion() {
if (mState != State.SHOWING_ICON && mState != State.HIDDEN) {
return;
}
if (getVisibility() == GONE) {
setVisibility(VISIBLE);
setWidth(0);
}
// Prepare views for the transition, these changes aren't animated.
mActionChipLabel.setVisibility(GONE);
mActionChipLabel.setText(mActionChipLabelString);
mAnimationImage.setImageDrawable(mButton.getDrawable());
ImageViewCompat.setImageTintList(
mAnimationImage, ImageViewCompat.getImageTintList(mButton));
mAnimationImage.setVisibility(VISIBLE);
mButton.setImageDrawable(mIconDrawable);
ImageViewCompat.setImageTintList(
mButton, mCurrentButtonSupportsTinting ? mForegroundColorTint : null);
mButton.setVisibility(GONE);
if (AdaptiveToolbarFeatures.shouldUseAlternativeActionChipColor(mCurrentButtonVariant)) {
int highlightColor = MaterialColors.getColor(this, R.attr.colorSecondaryContainer);
mBackground.setColorFilter(highlightColor);
} else {
mBackground.setColorFilter(mBackgroundColorFilter);
}
// Begin a transition, all layout changes after this call will be animated. The animation
// starts at the next frame.
beginDelayedTransition(createActionChipTransition());
mButton.setVisibility(VISIBLE);
mAnimationImage.setVisibility(GONE);
mActionChipLabel.setVisibility(VISIBLE);
mBackground.setVisibility(VISIBLE);
float actionChipLabelTextWidth =
mActionChipLabel.getPaint().measureText(mActionChipLabelString);
int maxExpandedStateWidthPx =
getResources()
.getDimensionPixelSize(
R.dimen.toolbar_phone_optional_button_action_chip_max_width);
int expandedStateWidthPx =
Math.min(
(int)
(mCollapsedStateWidthPx
+ actionChipLabelTextWidth
+ mExpandedStatePaddingPx),
maxExpandedStateWidthPx);
setWidth(expandedStateWidthPx);
mState = State.RUNNING_ACTION_CHIP_EXPANSION_TRANSITION;
}
private void animateActionChipCollapse() {
// Begin a transition, all layout changes after this call will be animated. The animation
// starts at the next frame.
beginDelayedTransition(createActionChipTransition());
mBackground.setColorFilter(mBackgroundColorFilter);
mActionChipLabel.setVisibility(GONE);
setWidth(mCollapsedStateWidthPx);
mState = State.RUNNING_ACTION_CHIP_COLLAPSE_TRANSITION;
}
private void hide(boolean animate) {
Transition transition = createShowHideTransition();
if (!animate) {
transition.setDuration(0);
}
// Begin a transition, all layout changes after this call will be animated. The animation
// starts at the next frame.
beginDelayedTransition(transition);
mButton.setVisibility(GONE);
mBackground.setVisibility(GONE);
mActionChipLabel.setVisibility(GONE);
setWidth(0);
if (mOnBeforeHideTransitionCallback != null) {
mOnBeforeHideTransitionCallback.run();
}
mState = State.RUNNING_HIDE_TRANSITION;
}
@Override
public void onRtlPropertiesChanged(int layoutDirection) {
if (mButton == null || mAnimationImage == null) return;
// ImageView's scale type does not take into account the layout's direction, FIT_START
// always aligns from the left and FIT_END always aligns from the right.
if (layoutDirection == LAYOUT_DIRECTION_LTR) {
mButton.setScaleType(ScaleType.FIT_START);
mAnimationImage.setScaleType(ScaleType.FIT_START);
} else {
mButton.setScaleType(ScaleType.FIT_END);
mAnimationImage.setScaleType(ScaleType.FIT_END);
}
}
private void showIcon(boolean animate) {
Transition transition = createShowHideTransition();
if (!animate) {
transition.setDuration(0);
}
// Prepare views for the transition, these changes aren't animated.
this.setVisibility(VISIBLE);
setWidth(0);
mButton.setVisibility(GONE);
mBackground.setVisibility(GONE);
mAnimationImage.setVisibility(GONE);
mActionChipLabel.setVisibility(GONE);
mButton.setImageDrawable(mIconDrawable);
ImageViewCompat.setImageTintList(
mButton, mCurrentButtonSupportsTinting ? mForegroundColorTint : null);
// Begin a transition, all layout changes after this call will be animated. The animation
// starts at the next frame.
beginDelayedTransition(transition);
setWidth(mCollapsedStateWidthPx);
mButton.setVisibility(VISIBLE);
mBackground.setColorFilter(mBackgroundColorFilter);
mBackground.setVisibility(mNextButtonType == ButtonType.DYNAMIC ? VISIBLE : GONE);
mState = State.RUNNING_SHOW_TRANSITION;
}
public void setFakeBeginDelayedTransitionForTesting(
Callback<Transition> fakeBeginDelayedTransition) {
mFakeBeginTransitionForTesting = fakeBeginDelayedTransition;
}
private void beginDelayedTransition(Transition transition) {
if (mFakeBeginTransitionForTesting != null) {
mFakeBeginTransitionForTesting.onResult(transition);
return;
}
TransitionManager.beginDelayedTransition(mTransitionRoot, transition);
}
}