From 0a4382496dad919f7bd3ba349d664f79c42851fc Mon Sep 17 00:00:00 2001 From: Fabio Carballo Date: Thu, 25 Jan 2024 04:24:08 -0800 Subject: [PATCH] Convert fbandroid/libraries/components/litho-core/src/main/java/com/facebook/litho/LithoViewAttributesExtension to Kotlin Summary: As per title Reviewed By: zielinskimz Differential Revision: D52997728 fbshipit-source-id: 764899397376ca55627d0f4ca39bb6700ef25be0 --- .../litho/LithoViewAttributesExtension.java | 993 ------------------ .../litho/LithoViewAttributesExtension.kt | 882 ++++++++++++++++ 2 files changed, 882 insertions(+), 993 deletions(-) delete mode 100644 litho-core/src/main/java/com/facebook/litho/LithoViewAttributesExtension.java create mode 100644 litho-core/src/main/java/com/facebook/litho/LithoViewAttributesExtension.kt diff --git a/litho-core/src/main/java/com/facebook/litho/LithoViewAttributesExtension.java b/litho-core/src/main/java/com/facebook/litho/LithoViewAttributesExtension.java deleted file mode 100644 index 5ef7d12b764..00000000000 --- a/litho-core/src/main/java/com/facebook/litho/LithoViewAttributesExtension.java +++ /dev/null @@ -1,993 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * 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.facebook.litho; - -import static androidx.core.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; -import static com.facebook.litho.ComponentHost.COMPONENT_NODE_INFO_ID; -import static com.facebook.litho.LithoMountData.isViewClickable; -import static com.facebook.litho.LithoMountData.isViewEnabled; -import static com.facebook.litho.LithoMountData.isViewFocusable; -import static com.facebook.litho.LithoMountData.isViewKeyboardNavigationCluster; -import static com.facebook.litho.LithoMountData.isViewLongClickable; -import static com.facebook.litho.LithoMountData.isViewSelected; -import static com.facebook.rendercore.MountState.ROOT_HOST_ID; - -import android.animation.AnimatorInflater; -import android.animation.StateListAnimator; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.text.TextUtils; -import android.util.SparseArray; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import androidx.annotation.ColorInt; -import androidx.annotation.IdRes; -import androidx.annotation.Nullable; -import androidx.core.view.ViewCompat; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.litho.LithoViewAttributesExtension.LithoViewAttributesState; -import com.facebook.litho.LithoViewAttributesExtension.ViewAttributesInput; -import com.facebook.rendercore.ErrorReporter; -import com.facebook.rendercore.LogLevel; -import com.facebook.rendercore.RenderTreeNode; -import com.facebook.rendercore.RenderUnit; -import com.facebook.rendercore.extensions.ExtensionState; -import com.facebook.rendercore.extensions.MountExtension; -import com.facebook.rendercore.extensions.OnItemCallbacks; -import com.facebook.rendercore.utils.EquivalenceUtils; -import java.util.HashMap; -import java.util.Map; - -@Nullsafe(Nullsafe.Mode.LOCAL) -public class LithoViewAttributesExtension - extends MountExtension - implements OnItemCallbacks { - - private static final LithoViewAttributesExtension sInstance = new LithoViewAttributesExtension(); - - private LithoViewAttributesExtension() {} - - static LithoViewAttributesExtension getInstance() { - return sInstance; - } - - @Override - protected LithoViewAttributesState createState() { - return new LithoViewAttributesState(); - } - - static class LithoViewAttributesState { - private Map mDefaultViewAttributes = new HashMap<>(); - private @Nullable Map mCurentUnits; - private @Nullable Map mNewUnits; - - void setDefaultViewAttributes(long renderUnitId, int flags) { - mDefaultViewAttributes.put(renderUnitId, flags); - } - - int getDefaultViewAttributes(long renderUnitId) { - final Integer flags = mDefaultViewAttributes.get(renderUnitId); - if (flags == null) { - throw new IllegalStateException( - "View attributes not found, did you call onUnbindItem without onBindItem?"); - } - - return flags; - } - - boolean hasDefaultViewAttributes(long renderUnitId) { - return mDefaultViewAttributes.containsKey(renderUnitId); - } - - @Nullable - ViewAttributes getCurrentViewAttributes(long id) { - return mCurentUnits != null ? mCurentUnits.get(id) : null; - } - - @Nullable - ViewAttributes getNewViewAttributes(long id) { - return mNewUnits != null ? mNewUnits.get(id) : null; - } - } - - @Override - public void beforeMount( - final ExtensionState extensionState, - final @Nullable ViewAttributesInput viewAttributesInput, - final @Nullable Rect localVisibleRect) { - if (viewAttributesInput != null) { - extensionState.getState().mNewUnits = viewAttributesInput.getViewAttributes(); - } - } - - @Override - public void afterMount(ExtensionState extensionState) { - extensionState.getState().mCurentUnits = extensionState.getState().mNewUnits; - } - - @Override - public void onMountItem( - final ExtensionState extensionState, - final RenderUnit renderUnit, - final Object content, - final @Nullable Object layoutData) { - final LithoViewAttributesState state = extensionState.getState(); - final long id = renderUnit.getId(); - final @Nullable ViewAttributes viewAttributes = state.getNewViewAttributes(id); - - if (viewAttributes != null) { - // Get the initial view attribute flags for the root LithoView. - if (!state.hasDefaultViewAttributes(id)) { - final int flags; - if (renderUnit.getId() == ROOT_HOST_ID) { - flags = ((BaseMountingView) content).mViewAttributeFlags; - } else { - flags = LithoMountData.getViewAttributeFlags(content); - } - state.setDefaultViewAttributes(id, flags); - } - setViewAttributes(content, viewAttributes, renderUnit); - } - } - - @Override - public void onUnmountItem( - final ExtensionState extensionState, - final RenderUnit renderUnit, - final Object content, - final @Nullable Object layoutData) { - final LithoViewAttributesState state = extensionState.getState(); - final long id = renderUnit.getId(); - final @Nullable ViewAttributes viewAttributes = state.getCurrentViewAttributes(id); - - if (viewAttributes != null) { - final int flags = state.getDefaultViewAttributes(id); - unsetViewAttributes(content, viewAttributes, flags); - } - } - - @Override - public void beforeMountItem( - ExtensionState extensionState, - RenderTreeNode renderTreeNode, - int index) {} - - @Override - public void onBindItem( - ExtensionState extensionState, - RenderUnit renderUnit, - Object content, - @Nullable Object layoutData) {} - - @Override - public void onUnbindItem( - ExtensionState extensionState, - RenderUnit renderUnit, - Object content, - @Nullable Object layoutData) {} - - @Override - public void onBoundsAppliedToItem( - ExtensionState extensionState, - RenderUnit renderUnit, - Object content, - @Nullable Object layoutData) {} - - @Override - public boolean shouldUpdateItem( - final ExtensionState extensionState, - final RenderUnit previousRenderUnit, - final @Nullable Object previousLayoutData, - final RenderUnit nextRenderUnit, - final @Nullable Object nextLayoutData) { - if (previousRenderUnit == nextRenderUnit) { - return false; - } - - final long id = previousRenderUnit.getId(); - final LithoViewAttributesState state = extensionState.getState(); - final @Nullable ViewAttributes currentAttributes = state.getCurrentViewAttributes(id); - final @Nullable ViewAttributes nextAttributes = state.getNewViewAttributes(id); - if (previousRenderUnit instanceof LithoRenderUnit - && nextRenderUnit instanceof LithoRenderUnit) { - return (previousRenderUnit instanceof MountSpecLithoRenderUnit - && nextRenderUnit instanceof MountSpecLithoRenderUnit - && MountSpecLithoRenderUnit.shouldUpdateMountItem( - (MountSpecLithoRenderUnit) previousRenderUnit, - (MountSpecLithoRenderUnit) nextRenderUnit, - previousLayoutData, - nextLayoutData)) - || shouldUpdateViewInfo(nextAttributes, currentAttributes); - } else { - return shouldUpdateViewInfo(nextAttributes, currentAttributes); - } - } - - @Override - public void onUnmount(ExtensionState extensionState) { - extensionState.getState().mCurentUnits = null; - extensionState.getState().mNewUnits = null; - } - - static void setViewAttributes(Object content, ViewAttributes attributes, RenderUnit unit) { - if (!(content instanceof View)) { - return; - } - - final View view = (View) content; - - setClickHandler(attributes.getClickHandler(), view); - setLongClickHandler(attributes.getLongClickHandler(), view); - setFocusChangeHandler(attributes.getFocusChangeHandler(), view); - setTouchHandler(attributes.getTouchHandler(), view); - setInterceptTouchHandler(attributes.getInterceptTouchHandler(), view); - - if (unit instanceof LithoRenderUnit) { - final NodeInfo nodeInfo = ((LithoRenderUnit) unit).getNodeInfo(); - if (nodeInfo != null) setAccessibilityDelegate(view, nodeInfo); - } - - setViewId(view, attributes.getViewId()); - if (attributes.isTagSet()) { - setViewTag(view, attributes.getViewTag()); - } - setViewTags(view, attributes.getViewTags()); - - setShadowElevation(view, attributes.getShadowElevation()); - setAmbientShadowColor(view, attributes.getAmbientShadowColor()); - setSpotShadowColor(view, attributes.getSpotShadowColor()); - setOutlineProvider(view, attributes.getOutlineProvider()); - setClipToOutline(view, attributes.getClipToOutline()); - setClipChildren(view, attributes); - - setContentDescription(view, attributes.getContentDescription()); - - setFocusable(view, attributes); - setClickable(view, attributes); - setEnabled(view, attributes); - setSelected(view, attributes); - setKeyboardNavigationCluster(view, attributes); - setScale(view, attributes); - setAlpha(view, attributes); - setRotation(view, attributes); - setRotationX(view, attributes); - setRotationY(view, attributes); - setTransitionName(view, attributes.getTransitionName()); - - setImportantForAccessibility(view, attributes.getImportantForAccessibility()); - - final boolean isHostSpec = attributes.isHostSpec(); - setViewLayerType(view, attributes); - setViewStateListAnimator(view, attributes); - if (attributes.getDisableDrawableOutputs()) { - setViewBackground(view, attributes); - ViewUtils.setViewForeground(view, attributes.getForeground()); - - // when background outputs are disabled, they are wrapped by a ComponentHost. - // A background can set the padding of a view, but ComponentHost should not have - // any padding because the layout calculation has already accounted for padding by - // translating the bounds of its children. - if (isHostSpec) { - view.setPadding(0, 0, 0, 0); - } - } - if (!isHostSpec) { - // Set view background, if applicable. Do this before padding - // as it otherwise overrides the padding. - setViewBackground(view, attributes); - - setViewPadding(view, attributes); - - ViewUtils.setViewForeground(view, attributes.getForeground()); - - setViewLayoutDirection(view, attributes); - } - } - - static void unsetViewAttributes( - final Object content, final ViewAttributes attributes, final int mountFlags) { - final boolean isHostView = attributes.isHostSpec(); - - if (!(content instanceof View)) { - return; - } - - final View view = (View) content; - - if (attributes.getClickHandler() != null) { - unsetClickHandler(view); - } - - if (attributes.getLongClickHandler() != null) { - unsetLongClickHandler(view); - } - - if (attributes.getFocusChangeHandler() != null) { - unsetFocusChangeHandler(view); - } - - if (attributes.getTouchHandler() != null) { - unsetTouchHandler(view); - } - - if (attributes.getInterceptTouchHandler() != null) { - unsetInterceptTouchEventHandler(view); - } - - if (attributes.isViewIdSet()) { - unsetViewId(view); - } - - if (attributes.isTagSet()) { - unsetViewTag(view); - } - unsetViewTags(view, attributes.getViewTags()); - - unsetShadowElevation(view, attributes.getShadowElevation()); - unsetAmbientShadowColor(view, attributes.getAmbientShadowColor()); - unsetSpotShadowColor(view, attributes.getSpotShadowColor()); - unsetOutlineProvider(view, attributes.getOutlineProvider()); - unsetClipToOutline(view, attributes.getClipToOutline()); - unsetClipChildren(view, attributes.getClipChildren()); - - if (!TextUtils.isEmpty(attributes.getContentDescription())) { - unsetContentDescription(view); - } - - unsetScale(view, attributes); - unsetAlpha(view, attributes); - unsetRotation(view, attributes); - unsetRotationX(view, attributes); - unsetRotationY(view, attributes); - - view.setClickable(isViewClickable(mountFlags)); - view.setLongClickable(isViewLongClickable(mountFlags)); - - unsetFocusable(view, mountFlags); - unsetEnabled(view, mountFlags); - unsetSelected(view, mountFlags); - unsetKeyboardNavigationCluster(view, mountFlags); - - if (attributes.getImportantForAccessibility() != IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - unsetImportantForAccessibility(view); - } - - unsetAccessibilityDelegate(view); - - unsetViewStateListAnimator(view, attributes); - // Host view doesn't set its own padding, but gets absolute positions for inner content from - // Yoga. Also bg/fg is used as separate drawables instead of using View's bg/fg attribute. - if (attributes.getDisableDrawableOutputs()) { - unsetViewBackground(view, attributes); - unsetViewForeground(view, attributes); - } - if (!isHostView) { - unsetViewPadding(view, attributes); - unsetViewBackground(view, attributes); - unsetViewForeground(view, attributes); - unsetViewLayoutDirection(view); - } - - unsetViewLayerType(view, mountFlags); - } - - /** - * Store a {@link NodeInfo} as a tag in {@code view}. {@link LithoView} contains the logic for - * setting/unsetting it whenever accessibility is enabled/disabled - * - *

For non {@link ComponentHost}s this is only done if any {@link EventHandler}s for - * accessibility events have been implemented, we want to preserve the original behaviour since - * {@code view} might have had a default delegate. - */ - private static void setAccessibilityDelegate(View view, NodeInfo nodeInfo) { - if (!(view instanceof ComponentHost) && !nodeInfo.needsAccessibilityDelegate()) { - return; - } - - view.setTag(COMPONENT_NODE_INFO_ID, nodeInfo); - } - - private static void unsetAccessibilityDelegate(View view) { - if (!(view instanceof ComponentHost) && view.getTag(COMPONENT_NODE_INFO_ID) == null) { - return; - } - view.setTag(COMPONENT_NODE_INFO_ID, null); - if (!(view instanceof ComponentHost)) { - ViewCompat.setAccessibilityDelegate(view, null); - } - } - - /** - * Installs the click listeners that will dispatch the click handler defined in the component's - * props. Unconditionally set the clickable flag on the view. - */ - private static void setClickHandler(@Nullable EventHandler clickHandler, View view) { - if (clickHandler == null) { - return; - } - - view.setOnClickListener(new ComponentClickListener(clickHandler)); - view.setClickable(true); - } - - private static void unsetClickHandler(View view) { - view.setOnClickListener(null); - view.setClickable(false); - } - - /** - * Installs the long click listeners that will dispatch the click handler defined in the - * component's props. Unconditionally set the clickable flag on the view. - */ - private static void setLongClickHandler( - @Nullable EventHandler longClickHandler, View view) { - if (longClickHandler != null) { - ComponentLongClickListener listener = getComponentLongClickListener(view); - - if (listener == null) { - listener = new ComponentLongClickListener(); - setComponentLongClickListener(view, listener); - } - - listener.setEventHandler(longClickHandler); - - view.setLongClickable(true); - } - } - - private static void unsetLongClickHandler(View view) { - final ComponentLongClickListener listener = getComponentLongClickListener(view); - - if (listener != null) { - listener.setEventHandler(null); - } - } - - @Nullable - static ComponentLongClickListener getComponentLongClickListener(View v) { - if (v instanceof ComponentHost) { - return ((ComponentHost) v).getComponentLongClickListener(); - } else { - return (ComponentLongClickListener) v.getTag(R.id.component_long_click_listener); - } - } - - static void setComponentLongClickListener(View v, ComponentLongClickListener listener) { - if (v instanceof ComponentHost) { - ((ComponentHost) v).setComponentLongClickListener(listener); - } else { - v.setOnLongClickListener(listener); - v.setTag(R.id.component_long_click_listener, listener); - } - } - - /** - * Installs the on focus change listeners that will dispatch the click handler defined in the - * component's props. Unconditionally set the clickable flag on the view. - */ - private static void setFocusChangeHandler( - @Nullable EventHandler focusChangeHandler, View view) { - if (focusChangeHandler == null) { - return; - } - - ComponentFocusChangeListener listener = getComponentFocusChangeListener(view); - - if (listener == null) { - listener = new ComponentFocusChangeListener(); - setComponentFocusChangeListener(view, listener); - } - - listener.setEventHandler(focusChangeHandler); - } - - private static void unsetFocusChangeHandler(View view) { - final ComponentFocusChangeListener listener = getComponentFocusChangeListener(view); - - if (listener != null) { - listener.setEventHandler(null); - } - } - - static @Nullable ComponentFocusChangeListener getComponentFocusChangeListener(View v) { - if (v instanceof ComponentHost) { - return ((ComponentHost) v).getComponentFocusChangeListener(); - } else { - return (ComponentFocusChangeListener) v.getTag(R.id.component_focus_change_listener); - } - } - - static void setComponentFocusChangeListener(View v, ComponentFocusChangeListener listener) { - if (v instanceof ComponentHost) { - ((ComponentHost) v).setComponentFocusChangeListener(listener); - } else { - v.setOnFocusChangeListener(listener); - v.setTag(R.id.component_focus_change_listener, listener); - } - } - - /** - * Installs the touch listeners that will dispatch the touch handler defined in the component's - * props. - */ - private static void setTouchHandler(@Nullable EventHandler touchHandler, View view) { - if (touchHandler != null) { - ComponentTouchListener listener = getComponentTouchListener(view); - - if (listener == null) { - listener = new ComponentTouchListener(); - setComponentTouchListener(view, listener); - } - - listener.setEventHandler(touchHandler); - } - } - - private static void unsetTouchHandler(View view) { - final ComponentTouchListener listener = getComponentTouchListener(view); - - if (listener != null) { - listener.setEventHandler(null); - } - } - - /** Sets the intercept touch handler defined in the component's props. */ - private static void setInterceptTouchHandler( - @Nullable EventHandler interceptTouchHandler, View view) { - if (interceptTouchHandler == null) { - return; - } - - if (view instanceof ComponentHost) { - ((ComponentHost) view).setInterceptTouchEventHandler(interceptTouchHandler); - } - } - - private static void unsetInterceptTouchEventHandler(View view) { - if (view instanceof ComponentHost) { - ((ComponentHost) view).setInterceptTouchEventHandler(null); - } - } - - @Nullable - static ComponentTouchListener getComponentTouchListener(View v) { - if (v instanceof ComponentHost) { - return ((ComponentHost) v).getComponentTouchListener(); - } else { - return (ComponentTouchListener) v.getTag(R.id.component_touch_listener); - } - } - - static void setComponentTouchListener(View v, ComponentTouchListener listener) { - if (v instanceof ComponentHost) { - ((ComponentHost) v).setComponentTouchListener(listener); - } else { - v.setOnTouchListener(listener); - v.setTag(R.id.component_touch_listener, listener); - } - } - - private static void setViewId(View view, @IdRes int id) { - if (id != View.NO_ID) { - view.setId(id); - } - } - - private static void unsetViewId(View view) { - view.setId(View.NO_ID); - } - - private static void setViewTag(View view, @Nullable Object viewTag) { - view.setTag(viewTag); - } - - private static void setViewTags(View view, @Nullable SparseArray viewTags) { - if (viewTags == null) { - return; - } - - if (view instanceof ComponentHost) { - final ComponentHost host = (ComponentHost) view; - host.setViewTags(viewTags); - } else { - for (int i = 0, size = viewTags.size(); i < size; i++) { - view.setTag(viewTags.keyAt(i), viewTags.valueAt(i)); - } - } - } - - private static void unsetViewTag(View view) { - view.setTag(null); - } - - private static void unsetViewTags(View view, @Nullable SparseArray viewTags) { - if (view instanceof ComponentHost) { - final ComponentHost host = (ComponentHost) view; - host.setViewTags(null); - } else { - if (viewTags != null) { - for (int i = 0, size = viewTags.size(); i < size; i++) { - view.setTag(viewTags.keyAt(i), null); - } - } - } - } - - private static void setShadowElevation(View view, float shadowElevation) { - if (shadowElevation != 0) { - ViewCompat.setElevation(view, shadowElevation); - } - } - - private static void setAmbientShadowColor(View view, @ColorInt int ambientShadowColor) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - view.setOutlineAmbientShadowColor(ambientShadowColor); - } - } - - private static void setSpotShadowColor(View view, @ColorInt int spotShadowColor) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - view.setOutlineSpotShadowColor(spotShadowColor); - } - } - - private static void unsetShadowElevation(View view, float shadowElevation) { - if (shadowElevation != 0) { - ViewCompat.setElevation(view, 0); - } - } - - private static void unsetAmbientShadowColor(View view, @ColorInt int ambientShadowColor) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && ambientShadowColor != Color.BLACK) { - // Android documentation says black is the default: - // https://developer.android.com/reference/android/view/View#getOutlineAmbientShadowColor() - view.setOutlineAmbientShadowColor(Color.BLACK); - } - } - - private static void unsetSpotShadowColor(View view, @ColorInt int spotShadowColor) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && spotShadowColor != Color.BLACK) { - // Android documentation says black is the default: - // https://developer.android.com/reference/android/view/View#getOutlineSpotShadowColor() - view.setOutlineSpotShadowColor(Color.BLACK); - } - } - - private static void setOutlineProvider(View view, @Nullable ViewOutlineProvider outlineProvider) { - if (outlineProvider != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - view.setOutlineProvider(outlineProvider); - } - } - - private static void unsetOutlineProvider( - View view, @Nullable ViewOutlineProvider outlineProvider) { - if (outlineProvider != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - view.setOutlineProvider(ViewOutlineProvider.BACKGROUND); - } - } - - private static void setClipToOutline(View view, boolean clipToOutline) { - if (clipToOutline && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - view.setClipToOutline(clipToOutline); - } - } - - private static void unsetClipToOutline(View view, boolean clipToOutline) { - if (clipToOutline && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - view.setClipToOutline(false); - } - } - - private static void setClipChildren(View view, ViewAttributes attributes) { - if (attributes.isClipChildrenSet() && view instanceof ViewGroup) { - ((ViewGroup) view).setClipChildren(attributes.getClipChildren()); - } - } - - private static void unsetClipChildren(View view, boolean clipChildren) { - if (!clipChildren && view instanceof ViewGroup) { - // Default value for clipChildren is 'true'. - // If this ViewGroup had clipChildren set to 'false' before mounting we would reset this - // property here on recycling. - ((ViewGroup) view).setClipChildren(true); - } - } - - private static void setContentDescription(View view, @Nullable CharSequence contentDescription) { - if (TextUtils.isEmpty(contentDescription)) { - return; - } - - view.setContentDescription(contentDescription); - } - - private static void unsetContentDescription(View view) { - view.setContentDescription(null); - } - - private static void setImportantForAccessibility(View view, int importantForAccessibility) { - if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - return; - } - - ViewCompat.setImportantForAccessibility(view, importantForAccessibility); - } - - private static void unsetImportantForAccessibility(View view) { - ViewCompat.setImportantForAccessibility(view, IMPORTANT_FOR_ACCESSIBILITY_AUTO); - } - - private static void setFocusable(View view, ViewAttributes attributes) { - if (attributes.isFocusableSet()) { - view.setFocusable(attributes.isFocusable()); - } - } - - private static void unsetFocusable(View view, int flags) { - view.setFocusable(isViewFocusable(flags)); - } - - private static void setTransitionName(View view, @Nullable String transitionName) { - if (transitionName != null) { - ViewCompat.setTransitionName(view, transitionName); - } - } - - private static void setClickable(View view, ViewAttributes attributes) { - if (attributes.isClickableSet()) { - view.setClickable(attributes.isClickable()); - } - } - - private static void setEnabled(View view, ViewAttributes attributes) { - if (attributes.isEnabledSet()) { - view.setEnabled(attributes.isEnabled()); - } - } - - private static void unsetEnabled(View view, int flags) { - view.setEnabled(isViewEnabled(flags)); - } - - private static void setSelected(View view, ViewAttributes attributes) { - if (attributes.isSelectedSet()) { - view.setSelected(attributes.isSelected()); - } - } - - private static void unsetSelected(View view, int flags) { - view.setSelected(isViewSelected(flags)); - } - - private static void setKeyboardNavigationCluster(View view, ViewAttributes attributes) { - if (attributes.isKeyboardNavigationClusterSet()) { - ViewCompat.setKeyboardNavigationCluster(view, attributes.isKeyboardNavigationCluster()); - } - } - - private static void unsetKeyboardNavigationCluster(View view, int flags) { - ViewCompat.setKeyboardNavigationCluster(view, isViewKeyboardNavigationCluster(flags)); - } - - private static void setScale(View view, ViewAttributes attributes) { - if (attributes.isScaleSet()) { - final float scale = attributes.getScale(); - view.setScaleX(scale); - view.setScaleY(scale); - } - } - - private static void unsetScale(View view, ViewAttributes attributes) { - if (attributes.isScaleSet()) { - if (view.getScaleX() != 1) { - view.setScaleX(1); - } - if (view.getScaleY() != 1) { - view.setScaleY(1); - } - } - } - - private static void setAlpha(View view, ViewAttributes attributes) { - if (attributes.isAlphaSet()) { - view.setAlpha(attributes.getAlpha()); - } - } - - private static void unsetAlpha(View view, ViewAttributes attributes) { - if (attributes.isAlphaSet() && view.getAlpha() != 1) { - view.setAlpha(1); - } - } - - private static void setRotation(View view, ViewAttributes attributes) { - if (attributes.isRotationSet()) { - view.setRotation(attributes.getRotation()); - } - } - - private static void unsetRotation(View view, ViewAttributes attributes) { - if (attributes.isRotationSet() && view.getRotation() != 0) { - view.setRotation(0); - } - } - - private static void setRotationX(View view, ViewAttributes attributes) { - if (attributes.isRotationXSet()) { - view.setRotationX(attributes.getRotationX()); - } - } - - private static void unsetRotationX(View view, ViewAttributes attributes) { - if (attributes.isRotationXSet() && view.getRotationX() != 0) { - view.setRotationX(0); - } - } - - private static void setRotationY(View view, ViewAttributes attributes) { - if (attributes.isRotationYSet()) { - view.setRotationY(attributes.getRotationY()); - } - } - - private static void unsetRotationY(View view, ViewAttributes attributes) { - if (attributes.isRotationYSet() && view.getRotationY() != 0) { - view.setRotationY(0); - } - } - - private static void setViewPadding(View view, ViewAttributes attributes) { - if (!attributes.hasPadding()) { - return; - } - - view.setPadding( - attributes.getPaddingLeft(), - attributes.getPaddingTop(), - attributes.getPaddingRight(), - attributes.getPaddingBottom()); - } - - private static void unsetViewPadding(View view, ViewAttributes attributes) { - if (!attributes.hasPadding()) { - return; - } - - try { - view.setPadding(0, 0, 0, 0); - } catch (NullPointerException e) { - // T53931759 Gathering extra info around this NPE - ErrorReporter.getInstance() - .report( - LogLevel.ERROR, - "LITHO:NPE:UNSET_PADDING", - "From component: " + attributes.getComponentName(), - e, - 0, - null); - } - } - - private static void setViewBackground(View view, ViewAttributes attributes) { - final Drawable background = attributes.getBackground(); - if (background != null) { - setBackgroundCompat(view, background); - } - } - - private static void unsetViewBackground(View view, ViewAttributes attributes) { - final Drawable background = attributes.getBackground(); - if (background != null) { - setBackgroundCompat(view, null); - } - } - - @SuppressWarnings("deprecation") - private static void setBackgroundCompat(View view, @Nullable Drawable drawable) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - view.setBackgroundDrawable(drawable); - } else { - view.setBackground(drawable); - } - } - - private static void unsetViewForeground(View view, ViewAttributes attributes) { - final Drawable foreground = attributes.getForeground(); - if (foreground != null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - throw new IllegalStateException( - "MountState has a ViewAttributes with foreground however " - + "the current Android version doesn't support foreground on Views"); - } - - view.setForeground(null); - } - } - - private static void setViewLayoutDirection(View view, ViewAttributes attributes) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { - return; - } - - view.setLayoutDirection(attributes.getLayoutDirection()); - } - - private static void unsetViewLayoutDirection(View view) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { - return; - } - - view.setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT); - } - - private static void setViewStateListAnimator(View view, ViewAttributes attributes) { - StateListAnimator stateListAnimator = attributes.getStateListAnimator(); - final int stateListAnimatorRes = attributes.getStateListAnimatorRes(); - if (stateListAnimator == null && stateListAnimatorRes == 0) { - return; - } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - throw new IllegalStateException( - "MountState has a ViewAttributes with stateListAnimator, " - + "however the current Android version doesn't support stateListAnimator on Views"); - } - if (stateListAnimator == null) { - stateListAnimator = - AnimatorInflater.loadStateListAnimator(view.getContext(), stateListAnimatorRes); - } - view.setStateListAnimator(stateListAnimator); - } - - private static void unsetViewStateListAnimator(View view, ViewAttributes attributes) { - if (attributes.getStateListAnimator() == null && attributes.getStateListAnimatorRes() == 0) { - return; - } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - throw new IllegalStateException( - "MountState has a ViewAttributes with stateListAnimator, " - + "however the current Android version doesn't support stateListAnimator on Views"); - } - view.setStateListAnimator(null); - } - - private static void setViewLayerType(final View view, final ViewAttributes attributes) { - final int type = attributes.getLayerType(); - if (type != LayerType.LAYER_TYPE_NOT_SET) { - view.setLayerType(attributes.getLayerType(), attributes.getLayoutPaint()); - } - } - - private static void unsetViewLayerType(final View view, final int mountFlags) { - int type = LithoMountData.getOriginalLayerType(mountFlags); - if (type != LayerType.LAYER_TYPE_NOT_SET) { - view.setLayerType(type, null); - } - } - - static boolean shouldUpdateViewInfo( - @Nullable final ViewAttributes nextAttributes, - @Nullable final ViewAttributes currentAttributes) { - return !EquivalenceUtils.equals(currentAttributes, nextAttributes); - } - - public interface ViewAttributesInput { - Map getViewAttributes(); - } -} diff --git a/litho-core/src/main/java/com/facebook/litho/LithoViewAttributesExtension.kt b/litho-core/src/main/java/com/facebook/litho/LithoViewAttributesExtension.kt new file mode 100644 index 00000000000..4b70c217a61 --- /dev/null +++ b/litho-core/src/main/java/com/facebook/litho/LithoViewAttributesExtension.kt @@ -0,0 +1,882 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * 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.facebook.litho + +import android.animation.AnimatorInflater +import android.graphics.Color +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.SparseArray +import android.view.View +import android.view.ViewGroup +import android.view.ViewOutlineProvider +import androidx.annotation.ColorInt +import androidx.annotation.IdRes +import androidx.core.view.ViewCompat +import com.facebook.litho.LithoViewAttributesExtension.LithoViewAttributesState +import com.facebook.litho.LithoViewAttributesExtension.ViewAttributesInput +import com.facebook.rendercore.ErrorReporter +import com.facebook.rendercore.LogLevel +import com.facebook.rendercore.MountState +import com.facebook.rendercore.RenderTreeNode +import com.facebook.rendercore.RenderUnit +import com.facebook.rendercore.extensions.ExtensionState +import com.facebook.rendercore.extensions.MountExtension +import com.facebook.rendercore.extensions.OnItemCallbacks +import com.facebook.rendercore.utils.equals + +class LithoViewAttributesExtension private constructor() : + MountExtension(), + OnItemCallbacks { + + interface ViewAttributesInput { + val viewAttributes: Map? + } + + override fun createState(): LithoViewAttributesState = LithoViewAttributesState() + + class LithoViewAttributesState { + private val _defaultViewAttributes: MutableMap = HashMap() + internal var currentUnits: Map? = null + internal var newUnits: Map? = null + + fun setDefaultViewAttributes(renderUnitId: Long, flags: Int) { + _defaultViewAttributes[renderUnitId] = flags + } + + fun getDefaultViewAttributes(renderUnitId: Long): Int { + return _defaultViewAttributes[renderUnitId] + ?: throw IllegalStateException( + "View attributes not found, did you call onUnbindItem without onBindItem?") + } + + fun hasDefaultViewAttributes(renderUnitId: Long): Boolean = + _defaultViewAttributes.containsKey(renderUnitId) + + fun getCurrentViewAttributes(id: Long): ViewAttributes? = currentUnits?.get(id) + + fun getNewViewAttributes(id: Long): ViewAttributes? = newUnits?.get(id) + } + + override fun beforeMount( + extensionState: ExtensionState, + input: ViewAttributesInput?, + localVisibleRect: Rect? + ) { + if (input != null) { + extensionState.state?.newUnits = input.viewAttributes + } + } + + override fun afterMount(extensionState: ExtensionState) { + extensionState.state.currentUnits = extensionState.state.newUnits + } + + override fun onMountItem( + extensionState: ExtensionState, + renderUnit: RenderUnit<*>, + content: Any, + layoutData: Any? + ) { + val state = extensionState.state + val id = renderUnit.id + val viewAttributes = state.getNewViewAttributes(id) + if (viewAttributes != null) { + // Get the initial view attribute flags for the root LithoView. + if (!state.hasDefaultViewAttributes(id)) { + val flags = + if (renderUnit.id == MountState.ROOT_HOST_ID) { + (content as BaseMountingView).mViewAttributeFlags + } else { + LithoMountData.getViewAttributeFlags(content) + } + state.setDefaultViewAttributes(id, flags) + } + setViewAttributes(content, viewAttributes, renderUnit) + } + } + + override fun onUnmountItem( + extensionState: ExtensionState, + renderUnit: RenderUnit<*>, + content: Any, + layoutData: Any? + ) { + val state = extensionState.state + val id = renderUnit.id + val viewAttributes = state.getCurrentViewAttributes(id) + if (viewAttributes != null) { + val flags = state.getDefaultViewAttributes(id) + unsetViewAttributes(content, viewAttributes, flags) + } + } + + override fun beforeMountItem( + extensionState: ExtensionState, + renderTreeNode: RenderTreeNode, + index: Int + ) = Unit + + override fun onBindItem( + extensionState: ExtensionState, + renderUnit: RenderUnit<*>, + content: Any, + layoutData: Any? + ) = Unit + + override fun onUnbindItem( + extensionState: ExtensionState, + renderUnit: RenderUnit<*>, + content: Any, + layoutData: Any? + ) = Unit + + override fun onBoundsAppliedToItem( + extensionState: ExtensionState, + renderUnit: RenderUnit<*>, + content: Any, + layoutData: Any? + ) = Unit + + override fun shouldUpdateItem( + extensionState: ExtensionState, + previousRenderUnit: RenderUnit<*>, + previousLayoutData: Any?, + nextRenderUnit: RenderUnit<*>, + nextLayoutData: Any? + ): Boolean { + if (previousRenderUnit === nextRenderUnit) { + return false + } + val id = previousRenderUnit.id + val state = extensionState.state + val currentAttributes = state.getCurrentViewAttributes(id) + val nextAttributes = state.getNewViewAttributes(id) + return (previousRenderUnit is MountSpecLithoRenderUnit && + nextRenderUnit is MountSpecLithoRenderUnit && + MountSpecLithoRenderUnit.shouldUpdateMountItem( + previousRenderUnit, nextRenderUnit, previousLayoutData, nextLayoutData)) || + shouldUpdateViewInfo(nextAttributes, currentAttributes) + } + + override fun onUnmount(extensionState: ExtensionState) { + extensionState.state.currentUnits = null + extensionState.state.newUnits = null + } + + companion object { + @get:JvmStatic val instance: LithoViewAttributesExtension = LithoViewAttributesExtension() + + @JvmStatic + fun setViewAttributes(content: Any?, attributes: ViewAttributes, unit: RenderUnit<*>?) { + if (content !is View) { + return + } + setClickHandler(attributes.clickHandler, content) + setLongClickHandler(attributes.longClickHandler, content) + setFocusChangeHandler(attributes.focusChangeHandler, content) + setTouchHandler(attributes.touchHandler, content) + setInterceptTouchHandler(attributes.interceptTouchHandler, content) + if (unit is LithoRenderUnit) { + val nodeInfo = unit.nodeInfo + if (nodeInfo != null) setAccessibilityDelegate(content, nodeInfo) + } + setViewId(content, attributes.viewId) + if (attributes.isTagSet) { + setViewTag(content, attributes.viewTag) + } + setViewTags(content, attributes.viewTags) + setShadowElevation(content, attributes.shadowElevation) + setAmbientShadowColor(content, attributes.ambientShadowColor) + setSpotShadowColor(content, attributes.spotShadowColor) + setOutlineProvider(content, attributes.outlineProvider) + setClipToOutline(content, attributes.clipToOutline) + setClipChildren(content, attributes) + setContentDescription(content, attributes.contentDescription) + setFocusable(content, attributes) + setClickable(content, attributes) + setEnabled(content, attributes) + setSelected(content, attributes) + setKeyboardNavigationCluster(content, attributes) + setScale(content, attributes) + setAlpha(content, attributes) + setRotation(content, attributes) + setRotationX(content, attributes) + setRotationY(content, attributes) + setTransitionName(content, attributes.transitionName) + setImportantForAccessibility(content, attributes.importantForAccessibility) + val isHostSpec = attributes.isHostSpec + setViewLayerType(content, attributes) + setViewStateListAnimator(content, attributes) + if (attributes.disableDrawableOutputs) { + setViewBackground(content, attributes) + setViewForeground(content, attributes.foreground) + + // when background outputs are disabled, they are wrapped by a ComponentHost. + // A background can set the padding of a view, but ComponentHost should not have + // any padding because the layout calculation has already accounted for padding by + // translating the bounds of its children. + if (isHostSpec) { + content.setPadding(0, 0, 0, 0) + } + } + if (!isHostSpec) { + // Set view background, if applicable. Do this before padding + // as it otherwise overrides the padding. + setViewBackground(content, attributes) + setViewPadding(content, attributes) + setViewForeground(content, attributes.foreground) + setViewLayoutDirection(content, attributes) + } + } + + @JvmStatic + fun unsetViewAttributes(content: Any?, attributes: ViewAttributes, mountFlags: Int) { + val isHostView = attributes.isHostSpec + if (content !is View) { + return + } + if (attributes.clickHandler != null) { + unsetClickHandler(content) + } + if (attributes.longClickHandler != null) { + unsetLongClickHandler(content) + } + if (attributes.focusChangeHandler != null) { + unsetFocusChangeHandler(content) + } + if (attributes.touchHandler != null) { + unsetTouchHandler(content) + } + if (attributes.interceptTouchHandler != null) { + unsetInterceptTouchEventHandler(content) + } + if (attributes.isViewIdSet) { + unsetViewId(content) + } + if (attributes.isTagSet) { + unsetViewTag(content) + } + unsetViewTags(content, attributes.viewTags) + unsetShadowElevation(content, attributes.shadowElevation) + unsetAmbientShadowColor(content, attributes.ambientShadowColor) + unsetSpotShadowColor(content, attributes.spotShadowColor) + unsetOutlineProvider(content, attributes.outlineProvider) + unsetClipToOutline(content, attributes.clipToOutline) + unsetClipChildren(content, attributes.clipChildren) + if (!attributes.contentDescription.isNullOrEmpty()) { + unsetContentDescription(content) + } + unsetScale(content, attributes) + unsetAlpha(content, attributes) + unsetRotation(content, attributes) + unsetRotationX(content, attributes) + unsetRotationY(content, attributes) + content.isClickable = LithoMountData.isViewClickable(mountFlags) + content.isLongClickable = LithoMountData.isViewLongClickable(mountFlags) + unsetFocusable(content, mountFlags) + unsetEnabled(content, mountFlags) + unsetSelected(content, mountFlags) + unsetKeyboardNavigationCluster(content, mountFlags) + if (attributes.importantForAccessibility != ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + unsetImportantForAccessibility(content) + } + unsetAccessibilityDelegate(content) + unsetViewStateListAnimator(content, attributes) + // Host view doesn't set its own padding, but gets absolute positions for inner content from + // Yoga. Also bg/fg is used as separate drawables instead of using View's bg/fg attribute. + if (attributes.disableDrawableOutputs) { + unsetViewBackground(content, attributes) + unsetViewForeground(content, attributes) + } + if (!isHostView) { + unsetViewPadding(content, attributes) + unsetViewBackground(content, attributes) + unsetViewForeground(content, attributes) + unsetViewLayoutDirection(content) + } + unsetViewLayerType(content, mountFlags) + } + /** + * Store a [NodeInfo] as a tag in `view`. [LithoView] contains the logic for setting/unsetting + * it whenever accessibility is enabled/disabled + * + * For non [ComponentHost]s this is only done if any [EventHandler]s for accessibility events + * have been implemented, we want to preserve the original behaviour since `view` might have had + * a default delegate. + */ + private fun setAccessibilityDelegate(view: View, nodeInfo: NodeInfo) { + if (view !is ComponentHost && !nodeInfo.needsAccessibilityDelegate()) { + return + } + view.setTag(ComponentHost.COMPONENT_NODE_INFO_ID, nodeInfo) + } + + private fun unsetAccessibilityDelegate(view: View) { + if (view !is ComponentHost && view.getTag(ComponentHost.COMPONENT_NODE_INFO_ID) == null) { + return + } + view.setTag(ComponentHost.COMPONENT_NODE_INFO_ID, null) + if (view !is ComponentHost) { + ViewCompat.setAccessibilityDelegate(view, null) + } + } + /** + * Installs the click listeners that will dispatch the click handler defined in the component's + * props. Unconditionally set the clickable flag on the view. + */ + private fun setClickHandler(clickHandler: EventHandler?, view: View) { + if (clickHandler == null) { + return + } + view.setOnClickListener(ComponentClickListener(clickHandler)) + view.isClickable = true + } + + private fun unsetClickHandler(view: View) { + view.setOnClickListener(null) + view.isClickable = false + } + /** + * Installs the long click listeners that will dispatch the click handler defined in the + * component's props. Unconditionally set the clickable flag on the view. + */ + private fun setLongClickHandler(longClickHandler: EventHandler?, view: View) { + if (longClickHandler != null) { + var listener = getComponentLongClickListener(view) + if (listener == null) { + listener = ComponentLongClickListener() + setComponentLongClickListener(view, listener) + } + listener.eventHandler = longClickHandler + view.isLongClickable = true + } + } + + private fun unsetLongClickHandler(view: View) { + val listener = getComponentLongClickListener(view) + if (listener != null) { + listener.eventHandler = null + } + } + + @JvmStatic + fun getComponentLongClickListener(v: View): ComponentLongClickListener? = + if (v is ComponentHost) { + v.componentLongClickListener + } else { + v.getTag(R.id.component_long_click_listener) as? ComponentLongClickListener + } + + @JvmStatic + fun setComponentLongClickListener(v: View, listener: ComponentLongClickListener?) { + if (v is ComponentHost) { + v.componentLongClickListener = listener + } else { + v.setOnLongClickListener(listener) + v.setTag(R.id.component_long_click_listener, listener) + } + } + /** + * Installs the on focus change listeners that will dispatch the click handler defined in the + * component's props. Unconditionally set the clickable flag on the view. + */ + private fun setFocusChangeHandler( + focusChangeHandler: EventHandler?, + view: View + ) { + if (focusChangeHandler == null) { + return + } + var listener = getComponentFocusChangeListener(view) + if (listener == null) { + listener = ComponentFocusChangeListener() + setComponentFocusChangeListener(view, listener) + } + listener.eventHandler = focusChangeHandler + } + + private fun unsetFocusChangeHandler(view: View) { + val listener = getComponentFocusChangeListener(view) + if (listener != null) { + listener.eventHandler = null + } + } + + @JvmStatic + fun getComponentFocusChangeListener(v: View): ComponentFocusChangeListener? = + if (v is ComponentHost) { + v.componentFocusChangeListener + } else { + v.getTag(R.id.component_focus_change_listener) as? ComponentFocusChangeListener + } + + @JvmStatic + fun setComponentFocusChangeListener(v: View, listener: ComponentFocusChangeListener?) { + if (v is ComponentHost) { + v.componentFocusChangeListener = listener + } else { + v.onFocusChangeListener = listener + v.setTag(R.id.component_focus_change_listener, listener) + } + } + /** + * Installs the touch listeners that will dispatch the touch handler defined in the component's + * props. + */ + private fun setTouchHandler(touchHandler: EventHandler?, view: View) { + if (touchHandler != null) { + var listener = getComponentTouchListener(view) + if (listener == null) { + listener = ComponentTouchListener() + setComponentTouchListener(view, listener) + } + listener.eventHandler = touchHandler + } + } + + private fun unsetTouchHandler(view: View) { + val listener = getComponentTouchListener(view) + if (listener != null) { + listener.eventHandler = null + } + } + /** Sets the intercept touch handler defined in the component's props. */ + private fun setInterceptTouchHandler( + interceptTouchHandler: EventHandler?, + view: View + ) { + if (interceptTouchHandler == null) { + return + } + if (view is ComponentHost) { + view.setInterceptTouchEventHandler(interceptTouchHandler) + } + } + + private fun unsetInterceptTouchEventHandler(view: View) { + if (view is ComponentHost) { + view.setInterceptTouchEventHandler(null) + } + } + + @JvmStatic + fun getComponentTouchListener(v: View): ComponentTouchListener? = + if (v is ComponentHost) { + v.componentTouchListener + } else { + v.getTag(R.id.component_touch_listener) as? ComponentTouchListener + } + + @JvmStatic + fun setComponentTouchListener(v: View, listener: ComponentTouchListener?) { + if (v is ComponentHost) { + v.componentTouchListener = listener + } else { + v.setOnTouchListener(listener) + v.setTag(R.id.component_touch_listener, listener) + } + } + + private fun setViewId(view: View, @IdRes id: Int) { + if (id != View.NO_ID) { + view.id = id + } + } + + private fun unsetViewId(view: View) { + view.id = View.NO_ID + } + + private fun setViewTag(view: View, viewTag: Any?) { + view.tag = viewTag + } + + private fun setViewTags(view: View, viewTags: SparseArray?) { + if (viewTags == null) { + return + } + if (view is ComponentHost) { + view.setViewTags(viewTags) + } else { + for (i in 0 until viewTags.size()) { + view.setTag(viewTags.keyAt(i), viewTags.valueAt(i)) + } + } + } + + private fun unsetViewTag(view: View) { + view.tag = null + } + + private fun unsetViewTags(view: View, viewTags: SparseArray?) { + if (view is ComponentHost) { + view.setViewTags(null) + } else { + if (viewTags != null) { + for (i in 0 until viewTags.size()) { + view.setTag(viewTags.keyAt(i), null) + } + } + } + } + + private fun setShadowElevation(view: View, shadowElevation: Float) { + if (shadowElevation != 0f) { + ViewCompat.setElevation(view, shadowElevation) + } + } + + private fun setAmbientShadowColor(view: View, @ColorInt ambientShadowColor: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + view.outlineAmbientShadowColor = ambientShadowColor + } + } + + private fun setSpotShadowColor(view: View, @ColorInt spotShadowColor: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + view.outlineSpotShadowColor = spotShadowColor + } + } + + private fun unsetShadowElevation(view: View, shadowElevation: Float) { + if (shadowElevation != 0f) { + ViewCompat.setElevation(view, 0f) + } + } + + private fun unsetAmbientShadowColor(view: View, @ColorInt ambientShadowColor: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && ambientShadowColor != Color.BLACK) { + // Android documentation says black is the default: + // https://developer.android.com/reference/android/view/View#getOutlineAmbientShadowColor() + view.outlineAmbientShadowColor = Color.BLACK + } + } + + private fun unsetSpotShadowColor(view: View, @ColorInt spotShadowColor: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && spotShadowColor != Color.BLACK) { + // Android documentation says black is the default: + // https://developer.android.com/reference/android/view/View#getOutlineSpotShadowColor() + view.outlineSpotShadowColor = Color.BLACK + } + } + + private fun setOutlineProvider(view: View, outlineProvider: ViewOutlineProvider?) { + if (outlineProvider != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.outlineProvider = outlineProvider + } + } + + private fun unsetOutlineProvider(view: View, outlineProvider: ViewOutlineProvider?) { + if (outlineProvider != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.outlineProvider = ViewOutlineProvider.BACKGROUND + } + } + + private fun setClipToOutline(view: View, clipToOutline: Boolean) { + if (clipToOutline && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.clipToOutline = clipToOutline + } + } + + private fun unsetClipToOutline(view: View, clipToOutline: Boolean) { + if (clipToOutline && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.clipToOutline = false + } + } + + private fun setClipChildren(view: View, attributes: ViewAttributes) { + if (attributes.isClipChildrenSet && view is ViewGroup) { + view.clipChildren = attributes.clipChildren + } + } + + private fun unsetClipChildren(view: View, clipChildren: Boolean) { + if (!clipChildren && view is ViewGroup) { + // Default value for clipChildren is 'true'. + // If this ViewGroup had clipChildren set to 'false' before mounting we would reset this + // property here on recycling. + view.clipChildren = true + } + } + + private fun setContentDescription(view: View, contentDescription: CharSequence?) { + if (contentDescription.isNullOrEmpty()) { + return + } + view.contentDescription = contentDescription + } + + private fun unsetContentDescription(view: View) { + view.contentDescription = null + } + + private fun setImportantForAccessibility(view: View, importantForAccessibility: Int) { + if (importantForAccessibility == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + return + } + ViewCompat.setImportantForAccessibility(view, importantForAccessibility) + } + + private fun unsetImportantForAccessibility(view: View) { + ViewCompat.setImportantForAccessibility(view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) + } + + private fun setFocusable(view: View, attributes: ViewAttributes) { + if (attributes.isFocusableSet) { + view.isFocusable = attributes.isFocusable + } + } + + private fun unsetFocusable(view: View, flags: Int) { + view.isFocusable = LithoMountData.isViewFocusable(flags) + } + + private fun setTransitionName(view: View, transitionName: String?) { + if (transitionName != null) { + ViewCompat.setTransitionName(view, transitionName) + } + } + + private fun setClickable(view: View, attributes: ViewAttributes) { + if (attributes.isClickableSet) { + view.isClickable = attributes.isClickable + } + } + + private fun setEnabled(view: View, attributes: ViewAttributes) { + if (attributes.isEnabledSet) { + view.isEnabled = attributes.isEnabled + } + } + + private fun unsetEnabled(view: View, flags: Int) { + view.isEnabled = LithoMountData.isViewEnabled(flags) + } + + private fun setSelected(view: View, attributes: ViewAttributes) { + if (attributes.isSelectedSet) { + view.isSelected = attributes.isSelected + } + } + + private fun unsetSelected(view: View, flags: Int) { + view.isSelected = LithoMountData.isViewSelected(flags) + } + + private fun setKeyboardNavigationCluster(view: View, attributes: ViewAttributes) { + if (attributes.isKeyboardNavigationClusterSet) { + ViewCompat.setKeyboardNavigationCluster(view, attributes.isKeyboardNavigationCluster) + } + } + + private fun unsetKeyboardNavigationCluster(view: View, flags: Int) { + ViewCompat.setKeyboardNavigationCluster( + view, LithoMountData.isViewKeyboardNavigationCluster(flags)) + } + + private fun setScale(view: View, attributes: ViewAttributes) { + if (attributes.isScaleSet) { + val scale = attributes.scale + view.scaleX = scale + view.scaleY = scale + } + } + + private fun unsetScale(view: View, attributes: ViewAttributes) { + if (attributes.isScaleSet) { + if (view.scaleX != 1f) { + view.scaleX = 1f + } + if (view.scaleY != 1f) { + view.scaleY = 1f + } + } + } + + private fun setAlpha(view: View, attributes: ViewAttributes) { + if (attributes.isAlphaSet) { + view.alpha = attributes.alpha + } + } + + private fun unsetAlpha(view: View, attributes: ViewAttributes) { + if (attributes.isAlphaSet && view.alpha != 1f) { + view.alpha = 1f + } + } + + private fun setRotation(view: View, attributes: ViewAttributes) { + if (attributes.isRotationSet) { + view.rotation = attributes.rotation + } + } + + private fun unsetRotation(view: View, attributes: ViewAttributes) { + if (attributes.isRotationSet && view.rotation != 0f) { + view.rotation = 0f + } + } + + private fun setRotationX(view: View, attributes: ViewAttributes) { + if (attributes.isRotationXSet) { + view.rotationX = attributes.rotationX + } + } + + private fun unsetRotationX(view: View, attributes: ViewAttributes) { + if (attributes.isRotationXSet && view.rotationX != 0f) { + view.rotationX = 0f + } + } + + private fun setRotationY(view: View, attributes: ViewAttributes) { + if (attributes.isRotationYSet) { + view.rotationY = attributes.rotationY + } + } + + private fun unsetRotationY(view: View, attributes: ViewAttributes) { + if (attributes.isRotationYSet && view.rotationY != 0f) { + view.rotationY = 0f + } + } + + private fun setViewPadding(view: View, attributes: ViewAttributes) { + if (!attributes.hasPadding()) { + return + } + view.setPadding( + attributes.paddingLeft, + attributes.paddingTop, + attributes.paddingRight, + attributes.paddingBottom) + } + + private fun unsetViewPadding(view: View, attributes: ViewAttributes) { + if (!attributes.hasPadding()) { + return + } + try { + view.setPadding(0, 0, 0, 0) + } catch (e: NullPointerException) { + // T53931759 Gathering extra info around this NPE + ErrorReporter.instance.report( + LogLevel.ERROR, + "LITHO:NPE:UNSET_PADDING", + "From component: ${attributes.componentName}", + e, + 0, + null) + } + } + + private fun setViewBackground(view: View, attributes: ViewAttributes) { + val background = attributes.background + if (background != null) { + setBackgroundCompat(view, background) + } + } + + private fun unsetViewBackground(view: View, attributes: ViewAttributes) { + val background = attributes.background + if (background != null) { + setBackgroundCompat(view, null) + } + } + + @Suppress("deprecation") + private fun setBackgroundCompat(view: View, drawable: Drawable?) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + view.setBackgroundDrawable(drawable) + } else { + view.background = drawable + } + } + + private fun unsetViewForeground(view: View, attributes: ViewAttributes) { + val foreground = attributes.foreground + if (foreground != null) { + check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + ("MountState has a ViewAttributes with foreground however the current Android version doesn't support foreground on Views") + } + view.foreground = null + } + } + + private fun setViewLayoutDirection(view: View, attributes: ViewAttributes) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return + } + view.layoutDirection = attributes.layoutDirection + } + + private fun unsetViewLayoutDirection(view: View) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return + } + view.layoutDirection = View.LAYOUT_DIRECTION_INHERIT + } + + private fun setViewStateListAnimator(view: View, attributes: ViewAttributes) { + var stateListAnimator = attributes.stateListAnimator + val stateListAnimatorRes = attributes.stateListAnimatorRes + if (stateListAnimator == null && stateListAnimatorRes == 0) { + return + } + check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ("MountState has a ViewAttributes with stateListAnimator, however the current Android version doesn't support stateListAnimator on Views") + } + if (stateListAnimator == null) { + stateListAnimator = + AnimatorInflater.loadStateListAnimator(view.context, stateListAnimatorRes) + } + view.stateListAnimator = stateListAnimator + } + + private fun unsetViewStateListAnimator(view: View, attributes: ViewAttributes) { + if (attributes.stateListAnimator == null && attributes.stateListAnimatorRes == 0) { + return + } + check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ("MountState has a ViewAttributes with stateListAnimator, however the current Android version doesn't support stateListAnimator on Views") + } + view.stateListAnimator = null + } + + private fun setViewLayerType(view: View, attributes: ViewAttributes) { + val type = attributes.layerType + if (type != LayerType.LAYER_TYPE_NOT_SET) { + view.setLayerType(attributes.layerType, attributes.layoutPaint) + } + } + + private fun unsetViewLayerType(view: View, mountFlags: Int) { + val type = LithoMountData.getOriginalLayerType(mountFlags) + if (type != LayerType.LAYER_TYPE_NOT_SET) { + view.setLayerType(type, null) + } + } + + @JvmStatic + fun shouldUpdateViewInfo( + nextAttributes: ViewAttributes?, + currentAttributes: ViewAttributes? + ): Boolean = !equals(currentAttributes, nextAttributes) + } +}