/* * Copyright (c) 2014-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.stetho.inspector.elements.android; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.android.FragmentCompatUtil; import com.facebook.stetho.inspector.elements.AbstractChainedDescriptor; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.Map; import java.util.WeakHashMap; import javax.annotation.Nullable; final class ViewGroupDescriptor extends AbstractChainedDescriptor<ViewGroup> implements HighlightableDescriptor<ViewGroup> { /** * This is a cache that maps from a View to the Fragment that contains it. If the View isn't * contained by a Fragment, then this maps the View to itself. For Views contained by Fragments, * we emit the Fragment instead, and then let the Fragment's descriptor emit the View as its sole * child. This allows us to see Fragments in the inspector as part of the UI tree. */ private final Map<View, Object> mViewToElementMap = Collections.synchronizedMap(new WeakHashMap<View, Object>()); public ViewGroupDescriptor() { } @Override protected void onGetChildren(ViewGroup element, Accumulator<Object> children) { for (int i = 0, N = element.getChildCount(); i < N; ++i) { final View childView = element.getChildAt(i); if (isChildVisible(childView)) { final Object childElement = getElementForView(element, childView); children.store(childElement); } } } private boolean isChildVisible(View child) { return !(child instanceof DocumentHiddenView); } private Object getElementForView(ViewGroup parentView, View childView) { Object value = mViewToElementMap.get(childView); if (value != null) { Object element = getElement(childView, value); // The parent of a View may have changed since we stashed it into the cache. // If that's the case then we can't use the cache's answer. if (element != null && childView.getParent() == parentView) { return element; } mViewToElementMap.remove(childView); } /** * Note that we do NOT emit DialogFragments. Those get emitted via ActivityDescriptor. * We do the check here so that we can also cache the cost of calling * {@link FragmentCompatUtil#isDialogFragment(Object)}. */ Object fragment = FragmentCompatUtil.findFragmentForView(childView); if (fragment != null && !FragmentCompatUtil.isDialogFragment(fragment)) { mViewToElementMap.put(childView, new WeakReference<>(fragment)); return fragment; } else { // No need to store a strong reference to the childView in the value. We'll just store this // object and when pull the value out of the map we'll check for this object and just use the // key instead. mViewToElementMap.put(childView, this); return childView; } } @SuppressWarnings("unchecked") private Object getElement(View childView, Object value) { if (value == this) { return childView; } else { return ((WeakReference<Object>) value).get(); } } @Override @Nullable public View getViewAndBoundsForHighlighting(ViewGroup element, Rect bounds) { return element; } @Nullable @Override public Object getElementToHighlightAtPosition(ViewGroup element, int x, int y, Rect bounds) { View hitChild = null; for (int i = element.getChildCount() - 1; i >= 0; --i) { final View childView = element.getChildAt(i); if (isChildVisible(childView) && childView.getVisibility() == View.VISIBLE) { childView.getHitRect(bounds); if (bounds.contains(x, y)) { hitChild = childView; break; } } } if (hitChild != null) { return hitChild; } else { bounds.set(0, 0, element.getWidth(), element.getHeight()); return element; } } }