/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.litho;
import java.util.ArrayList;
import java.util.List;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.util.SparseArrayCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.facebook.litho.R;
import static com.facebook.litho.AccessibilityUtils.isAccessibilityEnabled;
import static com.facebook.litho.ComponentHostUtils.maybeInvalidateAccessibilityState;
/**
* A {@link ViewGroup} that can host the mounted state of a {@link Component}. This is used
* by {@link MountState} to wrap mounted drawables to handle click events and update drawable
* states accordingly.
*/
public class ComponentHost extends ViewGroup {
private final SparseArrayCompat<MountItem> mMountItems = new SparseArrayCompat<>();
private SparseArrayCompat<MountItem> mScrapMountItemsArray;
private final SparseArrayCompat<MountItem> mViewMountItems = new SparseArrayCompat<>();
private SparseArrayCompat<MountItem> mScrapViewMountItemsArray;
private final SparseArrayCompat<MountItem> mDrawableMountItems = new SparseArrayCompat<>();
private SparseArrayCompat<MountItem> mScrapDrawableMountItems;
private final SparseArrayCompat<Touchable> mTouchables = new SparseArrayCompat<>();
private SparseArrayCompat<Touchable> mScrapTouchables;
private final SparseArrayCompat<MountItem> mDisappearingItems = new SparseArrayCompat<>();
private CharSequence mContentDescription;
private Object mViewTag;
private SparseArray<Object> mViewTags;
private boolean mWasInvalidatedWhileSuppressed;
private boolean mWasInvalidatedForAccessibilityWhileSuppressed;
private boolean mSuppressInvalidations;
private final InterleavedDispatchDraw mDispatchDraw = new InterleavedDispatchDraw();
private final List<ComponentHost> mScrapHosts = new ArrayList<>(3);
private final ComponentsLogger mLogger;
private int[] mChildDrawingOrder = new int[0];
private boolean mIsChildDrawingOrderDirty;
private long mParentHostMarker;
private boolean mInLayout;
private ComponentAccessibilityDelegate mComponentAccessibilityDelegate;
private boolean mIsComponentAccessibilityDelegateSet = false;
private ComponentClickListener mOnClickListener;
private ComponentLongClickListener mOnLongClickListener;
private ComponentTouchListener mOnTouchListener;
private EventHandler<InterceptTouchEvent> mOnInterceptTouchEventHandler;
private TouchExpansionDelegate mTouchExpansionDelegate;
public ComponentHost(Context context) {
this(context, null);
}
public ComponentHost(Context context, AttributeSet attrs) {
this(new ComponentContext(context), attrs);
}
public ComponentHost(ComponentContext context) {
this(context, null);
}
public ComponentHost(ComponentContext context, AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
setChildrenDrawingOrderEnabled(true);
mLogger = context.getLogger();
mComponentAccessibilityDelegate = new ComponentAccessibilityDelegate(this);
refreshAccessibilityDelegatesIfNeeded(isAccessibilityEnabled(context));
}
/**
* Sets the parent host marker for this host.
* @param parentHostMarker marker that indicates which {@link ComponentHost} hosts this host.
*/
void setParentHostMarker(long parentHostMarker) {
mParentHostMarker = parentHostMarker;
}
/**
* @return an id indicating which {@link ComponentHost} hosts this host.
*/
long getParentHostMarker() {
return mParentHostMarker;
}
/**
* Mounts the given {@link MountItem} with unique index.
* @param index index of the {@link MountItem}. Guaranteed to be the same index as is passed for
* the corresponding {@code unmount(index, mountItem)} call.
* @param mountItem item to be mounted into the host.
* @param bounds the bounds of the item that is to be mounted into the host
*/
public void mount(int index, MountItem mountItem, Rect bounds) {
final Object content = mountItem.getContent();
if (content instanceof Drawable) {
mountDrawable(index, mountItem, bounds);
} else if (content instanceof View) {
mViewMountItems.put(index, mountItem);
mountView((View) content, mountItem.getFlags());
maybeRegisterTouchExpansion(index, mountItem);
}
mMountItems.put(index, mountItem);
maybeInvalidateAccessibilityState(mountItem);
}
void unmount(MountItem item) {
final int index = mMountItems.keyAt(mMountItems.indexOfValue(item));
unmount(index, item);
}
/**
* Unmounts the given {@link MountItem} with unique index.
* @param index index of the {@link MountItem}. Guaranteed to be the same index as was passed for
* the corresponding {@code mount(index, mountItem)} call.
* @param mountItem item to be unmounted from the host.
*/
public void unmount(int index, MountItem mountItem) {
final Object content = mountItem.getContent();
if (content instanceof Drawable) {
unmountDrawable(index, mountItem);
} else if (content instanceof View) {
unmountView((View) content);
ComponentHostUtils.removeItem(index, mViewMountItems, mScrapViewMountItemsArray);
maybeUnregisterTouchExpansion(index, mountItem);
}
ComponentHostUtils.removeItem(index, mMountItems, mScrapMountItemsArray);
releaseScrapDataStructuresIfNeeded();
maybeInvalidateAccessibilityState(mountItem);
}
void startUnmountDisappearingItem(int index, MountItem mountItem) {
final Object content = mountItem.getContent();
if (!(content instanceof View)) {
throw new RuntimeException("Cannot unmount non-view item");
}
mIsChildDrawingOrderDirty = true;
ComponentHostUtils.removeItem(index, mViewMountItems, mScrapViewMountItemsArray);
ComponentHostUtils.removeItem(index, mMountItems, mScrapMountItemsArray);
releaseScrapDataStructuresIfNeeded();
mDisappearingItems.put(index, mountItem);
}
void unmountDisappearingItem(MountItem disappearingItem) {
final int indexOfValue = mDisappearingItems.indexOfValue(disappearingItem);
final int key = mDisappearingItems.keyAt(indexOfValue);
mDisappearingItems.removeAt(indexOfValue);
final View content = (View) disappearingItem.getContent();
unmountView(content);
maybeUnregisterTouchExpansion(key, disappearingItem);
maybeInvalidateAccessibilityState(disappearingItem);
}
boolean hasDisappearingItems() {
return mDisappearingItems.size() > 0;
}
List<String> getDisappearingItemKeys() {
if (!hasDisappearingItems()) {
return null;
}
final List<String> keys = new ArrayList<>();
for (int i = 0, size = mDisappearingItems.size(); i < size; i++) {
keys.add(mDisappearingItems.valueAt(i).getViewNodeInfo().getTransitionKey());
}
return keys;
}
private void maybeMoveTouchExpansionIndexes(MountItem item, int oldIndex, int newIndex) {
final ViewNodeInfo viewNodeInfo = item.getViewNodeInfo();
if (viewNodeInfo == null) {
return;
}
final Rect expandedTouchBounds = viewNodeInfo.getExpandedTouchBounds();
if (expandedTouchBounds == null || mTouchExpansionDelegate == null) {
return;
}
mTouchExpansionDelegate.moveTouchExpansionIndexes(
oldIndex,
newIndex);
}
private void maybeRegisterTouchExpansion(int index, MountItem mountItem) {
final ViewNodeInfo viewNodeInfo = mountItem.getViewNodeInfo();
if (viewNodeInfo == null) {
return;
}
final Rect expandedTouchBounds = viewNodeInfo.getExpandedTouchBounds();
if (expandedTouchBounds == null) {
return;
}
if (mTouchExpansionDelegate == null) {
mTouchExpansionDelegate = new TouchExpansionDelegate(this);
setTouchDelegate(mTouchExpansionDelegate);
}
mTouchExpansionDelegate.registerTouchExpansion(
index,
(View) mountItem.getContent(),
expandedTouchBounds);
}
private void maybeUnregisterTouchExpansion(int index, MountItem mountItem) {
final ViewNodeInfo viewNodeInfo = mountItem.getViewNodeInfo();
if (viewNodeInfo == null) {
return;
}
if (mTouchExpansionDelegate == null || viewNodeInfo.getExpandedTouchBounds() == null) {
return;
}
mTouchExpansionDelegate.unregisterTouchExpansion(index);
}
/**
* Tries to recycle a scrap host attached to this host.
* @return The host view to be recycled.
*/
ComponentHost recycleHost() {
if (mScrapHosts.size() > 0) {
final ComponentHost host = mScrapHosts.remove(0);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
// We are bringing the re-used host to the front because before API 17, Android doesn't
// take into account the children drawing order when dispatching ViewGroup touch events,
// but it just traverses its children list backwards.
bringChildToFront(host);
}
// The recycled host is immediately re-mounted in mountView(), therefore setting
// the flag here is redundant, but future proof.
mIsChildDrawingOrderDirty = true;
return host;
}
return null;
}
/**
* @return number of {@link MountItem}s that are currently mounted in the host.
*/
int getMountItemCount() {
return mMountItems.size();
}
/**
* @return the {@link MountItem} that was mounted with the given index.
*/
MountItem getMountItemAt(int index) {
return mMountItems.valueAt(index);
}
/**
* Hosts are guaranteed to have only one accessible component
* in them due to the way the view hierarchy is constructed in {@link LayoutState}.
* There might be other non-accessible components in the same hosts such as
* a background/foreground component though. This is why this method iterates over
* all mount items in order to find the accessible one.
*/
MountItem getAccessibleMountItem() {
for (int i = 0; i < getMountItemCount(); i++) {
MountItem item = getMountItemAt(i);
if (item.isAccessible()) {
return item;
}
}
return null;
}
/**
* @return list of drawables that are mounted on this host.
*/
public List<Drawable> getDrawables() {
final List<Drawable> drawables = new ArrayList<>(mDrawableMountItems.size());
for (int i = 0, size = mDrawableMountItems.size(); i < size; i++) {
Drawable drawable = (Drawable) mDrawableMountItems.valueAt(i).getContent();
drawables.add(drawable);
}
return drawables;
}
/**
* @return the text content that is mounted on this host.
*/
public TextContent getTextContent() {
return ComponentHostUtils.extractTextContent(
ComponentHostUtils.extractContent(mMountItems));
}
/**
* @return the image content that is mounted on this host.
*/
public ImageContent getImageContent() {
return ComponentHostUtils.extractImageContent(
ComponentHostUtils.extractContent(mMountItems));
}
/**
* @return the content descriptons that are set on content mounted on this host
*/
@Override
public CharSequence getContentDescription() {
return mContentDescription;
}
/**
* Host views implement their own content description handling instead of
* just delegating to the underlying view framework for performance reasons as
* the framework sets/resets content description very frequently on host views
* and the underlying accessibility notifications might cause performance issues.
* This is safe to do because the framework owns the accessibility state and
* knows how to update it efficiently.
*/
@Override
public void setContentDescription(CharSequence contentDescription) {
mContentDescription = contentDescription;
invalidateAccessibilityState();
}
@Override
public void setImportantForAccessibility(int mode) {
if (mode != ViewCompat.getImportantForAccessibility(this)) {
super.setImportantForAccessibility(mode);
}
}
@Override
public void setTag(int key, Object tag) {
super.setTag(key, tag);
if (key == R.id.component_node_info && tag != null) {
mComponentAccessibilityDelegate.setNodeInfo((NodeInfo) tag);
refreshAccessibilityDelegatesIfNeeded(isAccessibilityEnabled(getContext()));
}
}
/**
* Moves the MountItem associated to oldIndex in the newIndex position. This happens when a
* LithoView needs to re-arrange the internal order of its items. If an item is already
* present in newIndex the item is guaranteed to be either unmounted or moved to a different index
* by subsequent calls to either {@link ComponentHost#unmount(int, MountItem)} or
* {@link ComponentHost#moveItem(MountItem, int, int)}.
*
* @param item The item that has been moved.
* @param oldIndex The current index of the MountItem.
* @param newIndex The new index of the MountItem.
*/
void moveItem(MountItem item, int oldIndex, int newIndex) {
if (item == null && mScrapMountItemsArray != null) {
item = mScrapMountItemsArray.get(oldIndex);
}
if (item == null) {
return;
}
maybeMoveTouchExpansionIndexes(item, oldIndex, newIndex);
final Object content = item.getContent();
if (content instanceof Drawable) {
moveDrawableItem(item, oldIndex, newIndex);
} else if (content instanceof View) {
mIsChildDrawingOrderDirty = true;
startTemporaryDetach(((View) content));
if (mViewMountItems.get(newIndex) != null) {
ensureScrapViewMountItemsArray();
ComponentHostUtils.scrapItemAt(newIndex, mViewMountItems, mScrapViewMountItemsArray);
}
ComponentHostUtils.moveItem(oldIndex, newIndex, mViewMountItems, mScrapViewMountItemsArray);
}
if (mMountItems.get(newIndex) != null) {
ensureScrapMountItemsArray();
ComponentHostUtils.scrapItemAt(newIndex, mMountItems, mScrapMountItemsArray);
}
ComponentHostUtils.moveItem(oldIndex, newIndex, mMountItems, mScrapMountItemsArray);
releaseScrapDataStructuresIfNeeded();
if (content instanceof View) {
finishTemporaryDetach(((View) content));
}
}
/**
* Sets view tag on this host.
* @param viewTag the object to set as tag.
*/
public void setViewTag(Object viewTag) {
mViewTag = viewTag;
}
/**
* Sets view tags on this host.
* @param viewTags the map containing the tags by id.
*/
public void setViewTags(SparseArray<Object> viewTags) {
mViewTags = viewTags;
}
/**
* Sets a click listener on this host.
* @param listener The listener to set on this host.
*/
void setComponentClickListener(ComponentClickListener listener) {
mOnClickListener = listener;
this.setOnClickListener(listener);
}
/**
* @return The previously set click listener
*/
ComponentClickListener getComponentClickListener() {
return mOnClickListener;
}
/**
* Sets a long click listener on this host.
* @param listener The listener to set on this host.
*/
void setComponentLongClickListener(ComponentLongClickListener listener) {
mOnLongClickListener = listener;
this.setOnLongClickListener(listener);
}
/**
* @return The previously set long click listener
*/
ComponentLongClickListener getComponentLongClickListener() {
return mOnLongClickListener;
}
/**
* Sets a touch listener on this host.
* @param listener The listener to set on this host.
*/
void setComponentTouchListener(ComponentTouchListener listener) {
mOnTouchListener = listener;
setOnTouchListener(listener);
}
/**
* Sets an {@link EventHandler} that will be invoked when
* {@link ComponentHost#onInterceptTouchEvent} is called.
* @param interceptTouchEventHandler the handler to be set on this host.
*/
void setInterceptTouchEventHandler(EventHandler<InterceptTouchEvent> interceptTouchEventHandler) {
mOnInterceptTouchEventHandler = interceptTouchEventHandler;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mOnInterceptTouchEventHandler != null) {
return EventDispatcherUtils.dispatchOnInterceptTouch(mOnInterceptTouchEventHandler, ev);
}
return super.onInterceptTouchEvent(ev);
}
/**
* @return The previous set touch listener.
*/
public ComponentTouchListener getComponentTouchListener() {
return mOnTouchListener;
}
/**
* This is used to collapse all invalidation calls on hosts during mount.
* While invalidations are suppressed, the hosts will simply bail on
* invalidations. Once the suppression is turned off, a single invalidation
* will be triggered on the affected hosts.
*/
void suppressInvalidations(boolean suppressInvalidations) {
if (mSuppressInvalidations == suppressInvalidations) {
return;
}
mSuppressInvalidations = suppressInvalidations;
if (!mSuppressInvalidations) {
if (mWasInvalidatedWhileSuppressed) {
this.invalidate();
mWasInvalidatedWhileSuppressed = false;
}
if (mWasInvalidatedForAccessibilityWhileSuppressed) {
this.invalidateAccessibilityState();
mWasInvalidatedForAccessibilityWhileSuppressed = false;
}
}
}
/**
* Invalidates the accessibility node tree in this host.
*/
void invalidateAccessibilityState() {
if (!mIsComponentAccessibilityDelegateSet) {
return;
}
if (mSuppressInvalidations) {
mWasInvalidatedForAccessibilityWhileSuppressed = true;
return;
}
if (mComponentAccessibilityDelegate != null && implementsVirtualViews()) {
mComponentAccessibilityDelegate.invalidateRoot();
}
}
@Override
public boolean dispatchHoverEvent(MotionEvent event) {
return (mComponentAccessibilityDelegate != null
&& implementsVirtualViews()
&& mComponentAccessibilityDelegate.dispatchHoverEvent(event))
|| super.dispatchHoverEvent(event);
}
private boolean implementsVirtualViews() {
MountItem item = getAccessibleMountItem();
return item != null
&& item.getComponent().getLifecycle().implementsExtraAccessibilityNodes();
}
public List<CharSequence> getContentDescriptions() {
final List<CharSequence> contentDescriptions = new ArrayList<>();
for (int i = 0, size = mDrawableMountItems.size(); i < size; i++) {
final NodeInfo nodeInfo = mDrawableMountItems.valueAt(i).getNodeInfo();
if (nodeInfo == null) {
continue;
}
final CharSequence contentDescription = nodeInfo.getContentDescription();
if (contentDescription != null) {
contentDescriptions.add(contentDescription);
}
}
final CharSequence hostContentDescription = getContentDescription();
if (hostContentDescription != null) {
contentDescriptions.add(hostContentDescription);
}
return contentDescriptions;
}
private void mountView(View view, int flags) {
view.setDuplicateParentStateEnabled(MountItem.isDuplicateParentState(flags));
mIsChildDrawingOrderDirty = true;
// A host has been recycled and is already attached.
if (view instanceof ComponentHost && view.getParent() == this) {
finishTemporaryDetach(view);
view.setVisibility(VISIBLE);
return;
}
LayoutParams lp = view.getLayoutParams();
if (lp == null) {
lp = generateDefaultLayoutParams();
view.setLayoutParams(lp);
}
if (mInLayout) {
addViewInLayout(view, -1, view.getLayoutParams(), true);
} else {
addView(view, -1, view.getLayoutParams());
}
}
private void unmountView(View view) {
mIsChildDrawingOrderDirty = true;
if (view instanceof ComponentHost) {
final ComponentHost componentHost = (ComponentHost) view;
view.setVisibility(GONE);
// In Gingerbread the View system doesn't invalidate
// the parent if a child become invisible.
invalidate();
startTemporaryDetach(componentHost);
mScrapHosts.add(componentHost);
} else if (mInLayout) {
removeViewInLayout(view);
} else {
removeView(view);
}
}
TouchExpansionDelegate getTouchExpansionDelegate() {
return mTouchExpansionDelegate;
}
@Override
public void dispatchDraw(Canvas canvas) {
mDispatchDraw.start(canvas);
super.dispatchDraw(canvas);
// Cover the case where the host has no child views, in which case
// getChildDrawingOrder() will not be called and the draw index will not
// be incremented. This will also cover the case where drawables must be
// painted after the last child view in the host.
if (mDispatchDraw.isRunning()) {
mDispatchDraw.drawNext();
}
mDispatchDraw.end();
DebugDraw.draw(this, canvas);
}
@Override
protected int getChildDrawingOrder(int childCount, int i) {
updateChildDrawingOrderIfNeeded();
// This method is called in very different contexts within a ViewGroup
// e.g. when handling input events, drawing, etc. We only want to call
// the draw methods if the InterleavedDispatchDraw is active.
if (mDispatchDraw.isRunning()) {
mDispatchDraw.drawNext();
}
return mChildDrawingOrder[i];
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = false;
// Iterate drawable from last to first to respect drawing order.
for (int size = mTouchables.size(), i = size - 1; i >= 0; i--) {
final Touchable t = mTouchables.valueAt(i);
if (t.shouldHandleTouchEvent(event) && t.onTouchEvent(event, this)) {
handled = true;
break;
}
}
if (!handled) {
handled = super.onTouchEvent(event);
}
return handled;
}
void performLayout(boolean changed, int l, int t, int r, int b) {
}
@Override
protected final void onLayout(boolean changed, int l, int t, int r, int b) {
mInLayout = true;
performLayout(changed, l, t, r, b);
mInLayout = false;
}
@Override
public void requestLayout() {
// Don't request a layout if it will be blocked by any parent. Requesting a layout that is
// then ignored by an ancestor means that this host will remain in a state where it thinks that
// it has requested layout, and will therefore ignore future layout requests. This will lead to
// problems if a child (e.g. a ViewPager) requests a layout later on, since the request will be
// wrongly ignored by this host.
ViewParent parent = this;
while (parent instanceof ComponentHost) {
final ComponentHost host = (ComponentHost) parent;
if (!host.shouldRequestLayout()) {
return;
}
parent = parent.getParent();
}
super.requestLayout();
}
protected boolean shouldRequestLayout() {
// Don't bubble during layout.
return !mInLayout;
}
@Override
@SuppressLint("MissingSuperCall")
protected boolean verifyDrawable(Drawable who) {
return true;
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
for (int i = 0, size = mDrawableMountItems.size(); i < size; i++) {
final MountItem mountItem = mDrawableMountItems.valueAt(i);
ComponentHostUtils.maybeSetDrawableState(
this,
(Drawable) mountItem.getContent(),
mountItem.getFlags(),
mountItem.getNodeInfo());
}
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
for (int i = 0, size = mDrawableMountItems.size(); i < size; i++) {
final Drawable drawable = (Drawable) mDrawableMountItems.valueAt(i).getContent();
DrawableCompat.jumpToCurrentState(drawable);
}
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
for (int i = 0, size = mDrawableMountItems.size(); i < size; i++) {
final Drawable drawable = (Drawable) mDrawableMountItems.valueAt(i).getContent();
drawable.setVisible(visibility == View.VISIBLE, false);
}
}
@Override
public Object getTag() {
if (mViewTag != null) {
return mViewTag;
}
return super.getTag();
}
@Override
public Object getTag(int key) {
if (mViewTags != null) {
final Object value = mViewTags.get(key);
if (value != null) {
return value;
}
}
return super.getTag(key);
}
@Override
public void invalidate(Rect dirty) {
if (mSuppressInvalidations) {
mWasInvalidatedWhileSuppressed = true;
return;
}
super.invalidate(dirty);
}
@Override
public void invalidate(int l, int t, int r, int b) {
if (mSuppressInvalidations) {
mWasInvalidatedWhileSuppressed = true;
return;
}
super.invalidate(l, t, r, b);
}
@Override
public void invalidate() {
if (mSuppressInvalidations) {
mWasInvalidatedWhileSuppressed = true;
return;
}
super.invalidate();
}
protected void refreshAccessibilityDelegatesIfNeeded(boolean isAccessibilityEnabled) {
if (isAccessibilityEnabled == mIsComponentAccessibilityDelegateSet) {
return;
}
ViewCompat.setAccessibilityDelegate(
this,
isAccessibilityEnabled ? mComponentAccessibilityDelegate : null);
mIsComponentAccessibilityDelegateSet = isAccessibilityEnabled;
for (int i = 0, size = getChildCount(); i < size; i++) {
final View child = getChildAt(i);
if (child instanceof ComponentHost) {
((ComponentHost) child).refreshAccessibilityDelegatesIfNeeded(isAccessibilityEnabled);
} else {
final NodeInfo nodeInfo =
(NodeInfo) child.getTag(R.id.component_node_info);
if (nodeInfo != null) {
ViewCompat.setAccessibilityDelegate(
child,
isAccessibilityEnabled ? new ComponentAccessibilityDelegate(child, nodeInfo) : null);
}
}
}
}
@Override
public void setAccessibilityDelegate(AccessibilityDelegate accessibilityDelegate) {
super.setAccessibilityDelegate(accessibilityDelegate);
// We cannot compare against mComponentAccessibilityDelegate directly, since it is not the
// delegate that we receive here. Instead, we'll set this to true at the point that we set that
// delegate explicitly.
mIsComponentAccessibilityDelegateSet = false;
}
private void updateChildDrawingOrderIfNeeded() {
if (!mIsChildDrawingOrderDirty) {
return;
}
final int childCount = getChildCount();
if (mChildDrawingOrder.length < childCount) {
mChildDrawingOrder = new int[childCount + 5];
}
int index = 0;
final int viewMountItemCount = mViewMountItems.size();
for (int i = 0, size = viewMountItemCount; i < size; i++) {
final View child = (View) mViewMountItems.valueAt(i).getContent();
mChildDrawingOrder[index++] = indexOfChild(child);
}
// Draw disappearing items on top of mounted views.
for (int i = 0, size = mDisappearingItems.size(); i < size; i++) {
final View child = (View) mDisappearingItems.valueAt(i).getContent();
mChildDrawingOrder[index++] = indexOfChild(child);
}
for (int i = 0, size = mScrapHosts.size(); i < size; i++) {
final View child = mScrapHosts.get(i);
mChildDrawingOrder[index++] = indexOfChild(child);
}
mIsChildDrawingOrderDirty = false;
}
private void ensureScrapViewMountItemsArray() {
if (mScrapViewMountItemsArray == null) {
mScrapViewMountItemsArray = ComponentsPools.acquireScrapMountItemsArray();
}
}
private void ensureScrapMountItemsArray() {
if (mScrapMountItemsArray == null) {
mScrapMountItemsArray = ComponentsPools.acquireScrapMountItemsArray();
}
}
private void releaseScrapDataStructuresIfNeeded() {
if (mScrapMountItemsArray != null && mScrapMountItemsArray.size() == 0) {
ComponentsPools.releaseScrapMountItemsArray(mScrapMountItemsArray);
mScrapMountItemsArray = null;
}
if (mScrapViewMountItemsArray != null && mScrapViewMountItemsArray.size() == 0) {
ComponentsPools.releaseScrapMountItemsArray(mScrapViewMountItemsArray);
mScrapViewMountItemsArray = null;
}
}
private void mountDrawable(int index, MountItem mountItem, Rect bounds) {
mDrawableMountItems.put(index, mountItem);
final Drawable drawable = (Drawable) mountItem.getContent();
final DisplayListDrawable displayListDrawable = mountItem.getDisplayListDrawable();
ComponentHostUtils.mountDrawable(
this,
displayListDrawable != null ? displayListDrawable : drawable,
bounds,
mountItem.getFlags(),
mountItem.getNodeInfo());
if (drawable instanceof Touchable) {
mTouchables.put(index, (Touchable) drawable);
}
}
private void unmountDrawable(int index, MountItem mountItem) {
final Drawable contentDrawable = (Drawable) mountItem.getContent();
final Drawable drawable = mountItem.getDisplayListDrawable() == null
? contentDrawable
: mountItem.getDisplayListDrawable();
if (ComponentHostUtils.existsScrapItemAt(index, mScrapDrawableMountItems)) {
mScrapDrawableMountItems.remove(index);
} else {
mDrawableMountItems.remove(index);
}
drawable.setCallback(null);
if (contentDrawable instanceof Touchable) {
if (ComponentHostUtils.existsScrapItemAt(index, mScrapTouchables)) {
mScrapTouchables.remove(index);
} else {
mTouchables.remove(index);
}
}
this.invalidate(drawable.getBounds());
releaseScrapDataStructuresIfNeeded();
}
private void moveDrawableItem(MountItem item, int oldIndex, int newIndex) {
// When something is already present in newIndex position we need to keep track of it.
if (mDrawableMountItems.get(newIndex) != null) {
ensureScrapDrawableMountItemsArray();
ComponentHostUtils.scrapItemAt(newIndex, mDrawableMountItems, mScrapDrawableMountItems);
}
if (mTouchables.get(newIndex) != null) {
ensureScrapTouchablesArray();
ComponentHostUtils.scrapItemAt(newIndex, mTouchables, mScrapTouchables);
}
// Move the MountItem in the new position. If the mount item was a Touchable we need to reflect
// this change also in the Touchables SparseArray.
ComponentHostUtils.moveItem(oldIndex, newIndex, mDrawableMountItems, mScrapDrawableMountItems);
if (item.getContent() instanceof Touchable) {
ComponentHostUtils.moveItem(oldIndex, newIndex, mTouchables, mScrapTouchables);
}
// Drawing order changed, invalidate the whole view.
this.invalidate();
releaseScrapDataStructuresIfNeeded();
}
private void ensureScrapDrawableMountItemsArray() {
if (mScrapDrawableMountItems == null) {
mScrapDrawableMountItems = ComponentsPools.acquireScrapMountItemsArray();
}
}
private void ensureScrapTouchablesArray() {
if (mScrapTouchables == null) {
mScrapTouchables = ComponentsPools.acquireScrapTouchablesArray();
}
}
private static void startTemporaryDetach(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// Cancel any pending clicks.
view.cancelPendingInputEvents();
}
// The ComponentHost's parent will send an ACTION_CANCEL if it's going to receive
// other motion events for the recycled child.
ViewCompat.dispatchStartTemporaryDetach(view);
}
private static void finishTemporaryDetach(View view) {
ViewCompat.dispatchFinishTemporaryDetach(view);
}
/**
* Encapsulates the logic for drawing a set of views and drawables respecting
* their drawing order withing the component host i.e. allow interleaved views
* and drawables to be drawn with the correct z-index.
*/
private class InterleavedDispatchDraw {
private Canvas mCanvas;
private int mDrawIndex;
private int mItemsToDraw;
private InterleavedDispatchDraw() {
}
private void start(Canvas canvas) {
mCanvas = canvas;
mDrawIndex = 0;
mItemsToDraw = mMountItems.size();
}
private boolean isRunning() {
return (mCanvas != null && mDrawIndex < mItemsToDraw);
}
private void drawNext() {
if (mCanvas == null) {
return;
}
for (int i = mDrawIndex, size = mMountItems.size(); i < size; i++) {
final MountItem mountItem = mMountItems.valueAt(i);
final Object content = mountItem.getDisplayListDrawable() != null ?
mountItem.getDisplayListDrawable() :
mountItem.getContent();
// During a ViewGroup's dispatchDraw() call with children drawing order enabled,
// getChildDrawingOrder() will be called before each child view is drawn. This
// method will only draw the drawables "between" the child views and the let
// the host draw its children as usual. This is why views are skipped here.
if (content instanceof View) {
mDrawIndex = i + 1;
return;
}
ComponentsSystrace.beginSection(mountItem.getComponent().getSimpleName());
((Drawable) content).draw(mCanvas);
ComponentsSystrace.endSection();
}
mDrawIndex = mItemsToDraw;
}
private void end() {
mCanvas = null;
}
}
}