All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.layoutlib.bridge.impl.RenderSessionImpl Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.layoutlib.bridge.impl;

import com.android.ide.common.rendering.api.AdapterBinding;
import com.android.ide.common.rendering.api.HardwareConfig;
import com.android.ide.common.rendering.api.ILayoutLog;
import com.android.ide.common.rendering.api.ILayoutPullParser;
import com.android.ide.common.rendering.api.LayoutlibCallback;
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.ResourceReference;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.Result;
import com.android.ide.common.rendering.api.SessionParams;
import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
import com.android.ide.common.rendering.api.SessionParams.RenderingMode.SizeAction;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.ide.common.rendering.api.ViewType;
import com.android.internal.view.menu.ActionMenuItemView;
import com.android.internal.view.menu.BridgeMenuItemImpl;
import com.android.internal.view.menu.IconMenuItemView;
import com.android.internal.view.menu.ListMenuItemView;
import com.android.internal.view.menu.MenuItemImpl;
import com.android.internal.view.menu.MenuView;
import com.android.layoutlib.bridge.Bridge;
import com.android.layoutlib.bridge.android.BridgeContext;
import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
import com.android.layoutlib.bridge.android.RenderParamsFlags;
import com.android.layoutlib.bridge.android.graphics.NopCanvas;
import com.android.layoutlib.bridge.android.support.DesignLibUtil;
import com.android.layoutlib.bridge.android.support.FragmentTabHostUtil;
import com.android.layoutlib.bridge.android.support.SupportPreferencesUtil;
import com.android.layoutlib.bridge.impl.binding.FakeAdapter;
import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter;
import com.android.tools.idea.validator.LayoutValidator;
import com.android.tools.idea.validator.ValidatorResult;
import com.android.tools.idea.validator.ValidatorResult.Builder;
import com.android.tools.layoutlib.java.System_Delegate;
import com.android.utils.Pair;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Bitmap;
import android.graphics.Bitmap_Delegate;
import android.graphics.Canvas;
import android.graphics.NinePatch_Delegate;
import android.os.Looper;
import android.preference.Preference_Delegate;
import android.view.AttachInfo_Accessor;
import android.view.BridgeInflater;
import android.view.Choreographer_Delegate;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewParent;
import android.widget.AbsListView;
import android.widget.AbsSpinner;
import android.widget.ActionMenuView;
import android.widget.AdapterView;
import android.widget.ExpandableListView;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.QuickContactBadge;
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;
import android.widget.TabWidget;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;

import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchyAndroid_ViewElementClassNamesAndroid_Delegate;

import static com.android.ide.common.rendering.api.Result.Status.ERROR_INFLATION;
import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED;
import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
import static com.android.layoutlib.bridge.util.ReflectionUtils.isInstanceOf;

/**
 * Class implementing the render session.
 * 

* A session is a stateful representation of a layout file. It is initialized with data coming * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then * be done on the layout. */ public class RenderSessionImpl extends RenderAction { private static final Canvas NOP_CANVAS = new NopCanvas(); // scene state private RenderSession mScene; private BridgeXmlBlockParser mBlockParser; private BridgeInflater mInflater; private ViewGroup mViewRoot; private FrameLayout mContentRoot; private Canvas mCanvas; private int mMeasuredScreenWidth = -1; private int mMeasuredScreenHeight = -1; /** If >= 0, a frame will be executed */ private long mElapsedFrameTimeNanos = -1; /** True if one frame has been already executed to start the animations */ private boolean mFirstFrameExecuted = false; // information being returned through the API private BufferedImage mImage; private List mViewInfoList; private List mSystemViewInfoList; private Layout.Builder mLayoutBuilder; private boolean mNewRenderSize; @Nullable private ValidatorResult mValidatorResult = null; private static final class PostInflateException extends Exception { private static final long serialVersionUID = 1L; private PostInflateException(String message) { super(message); } } /** * Creates a layout scene with all the information coming from the layout bridge API. *

* This must be followed by a call to {@link RenderSessionImpl#init(long)}, * which act as a * call to {@link RenderSessionImpl#acquire(long)} * * @see Bridge#createSession(SessionParams) */ public RenderSessionImpl(SessionParams params) { super(new SessionParams(params)); } /** * Initializes and acquires the scene, creating various Android objects such as context, * inflater, and parser. * * @param timeout the time to wait if another rendering is happening. * * @return whether the scene was prepared * * @see #acquire(long) * @see #release() */ @Override public Result init(long timeout) { Result result = super.init(timeout); if (!result.isSuccess()) { return result; } SessionParams params = getParams(); BridgeContext context = getContext(); mLayoutBuilder = new Layout.Builder(params, context); // build the inflater and parser. mInflater = new BridgeInflater(context, params.getLayoutlibCallback()); context.setBridgeInflater(mInflater); ILayoutPullParser layoutParser = params.getLayoutDescription(); mBlockParser = new BridgeXmlBlockParser(layoutParser, context, layoutParser.getLayoutNamespace()); return SUCCESS.createResult(); } /** * Measures the the current layout if needed (see {@link #invalidateRenderingSize}). */ private void measureLayout(@NonNull SessionParams params) { // only do the screen measure when needed. if (mMeasuredScreenWidth != -1) { return; } RenderingMode renderingMode = params.getRenderingMode(); HardwareConfig hardwareConfig = params.getHardwareConfig(); mNewRenderSize = true; mMeasuredScreenWidth = hardwareConfig.getScreenWidth(); mMeasuredScreenHeight = hardwareConfig.getScreenHeight(); if (renderingMode != RenderingMode.NORMAL) { int widthMeasureSpecMode = renderingMode.getHorizAction() == SizeAction.EXPAND ? MeasureSpec.UNSPECIFIED // this lets us know the actual needed size : MeasureSpec.EXACTLY; int heightMeasureSpecMode = renderingMode.getVertAction() == SizeAction.EXPAND ? MeasureSpec.UNSPECIFIED // this lets us know the actual needed size : MeasureSpec.EXACTLY; // We used to compare the measured size of the content to the screen size but // this does not work anymore due to the 2 following issues: // - If the content is in a decor (system bar, title/action bar), the root view // will not resize even with the UNSPECIFIED because of the embedded layout. // - If there is no decor, but a dialog frame, then the dialog padding prevents // comparing the size of the content to the screen frame (as it would not // take into account the dialog padding). // The solution is to first get the content size in a normal rendering, inside // the decor or the dialog padding. // Then measure only the content with UNSPECIFIED to see the size difference // and apply this to the screen size. View measuredView = mContentRoot.getChildAt(0); // first measure the full layout, with EXACTLY to get the size of the // content as it is inside the decor/dialog Pair exactMeasure = measureView( mViewRoot, measuredView, mMeasuredScreenWidth, MeasureSpec.EXACTLY, mMeasuredScreenHeight, MeasureSpec.EXACTLY); // now measure the content only using UNSPECIFIED (where applicable, based on // the rendering mode). This will give us the size the content needs. Pair neededMeasure = measureView( mContentRoot, mContentRoot.getChildAt(0), mMeasuredScreenWidth, widthMeasureSpecMode, mMeasuredScreenHeight, heightMeasureSpecMode); int neededWidth = neededMeasure.getFirst(); int neededHeight = neededMeasure.getSecond(); // If measuredView is not null, exactMeasure nor result will be null. assert (exactMeasure != null && neededMeasure != null) || measuredView == null; // now look at the difference and add what is needed. if (renderingMode.getHorizAction() == SizeAction.EXPAND) { int measuredWidth = exactMeasure.getFirst(); if (neededWidth > measuredWidth) { mMeasuredScreenWidth += neededWidth - measuredWidth; } if (mMeasuredScreenWidth < measuredWidth) { // If the screen width is less than the exact measured width, // expand to match. mMeasuredScreenWidth = measuredWidth; } } else if (renderingMode.getHorizAction() == SizeAction.SHRINK) { mMeasuredScreenWidth = neededWidth; } if (renderingMode.getVertAction() == SizeAction.EXPAND) { int measuredHeight = exactMeasure.getSecond(); if (neededHeight > measuredHeight) { mMeasuredScreenHeight += neededHeight - measuredHeight; } if (mMeasuredScreenHeight < measuredHeight) { // If the screen height is less than the exact measured height, // expand to match. mMeasuredScreenHeight = measuredHeight; } } else if (renderingMode.getVertAction() == SizeAction.SHRINK) { mMeasuredScreenHeight = neededHeight; } } } /** * Inflates the layout. *

* {@link #acquire(long)} must have been called before this. * * @throws IllegalStateException if the current context is different than the one owned by * the scene, or if {@link #init(long)} was not called. */ public Result inflate() { checkLock(); try { mViewRoot = new Layout(mLayoutBuilder); mLayoutBuilder = null; // Done with the builder. mContentRoot = ((Layout) mViewRoot).getContentRoot(); SessionParams params = getParams(); BridgeContext context = getContext(); if (Bridge.isLocaleRtl(params.getLocale())) { if (!params.isRtlSupported()) { Bridge.getLog().warning(ILayoutLog.TAG_RTL_NOT_ENABLED, "You are using a right-to-left " + "(RTL) locale but RTL is not enabled", null, null); } else if (params.getSimulatedPlatformVersion() !=0 && params.getSimulatedPlatformVersion() < 17) { // This will render ok because we are using the latest layoutlib but at least // warn the user that this might fail in a real device. Bridge.getLog().warning(ILayoutLog.TAG_RTL_NOT_SUPPORTED, "You are using a " + "right-to-left " + "(RTL) locale but RTL is not supported for API level < 17", null, null); } } String rootTag = params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG); boolean isPreference = "PreferenceScreen".equals(rootTag) || SupportPreferencesUtil.isSupportRootTag(rootTag); View view; if (isPreference) { // First try to use the support library inflater. If something fails, fallback // to the system preference inflater. view = SupportPreferencesUtil.inflatePreference(context, mBlockParser, mContentRoot); if (view == null) { view = Preference_Delegate.inflatePreference(context, mBlockParser, mContentRoot); } } else { view = mInflater.inflate(mBlockParser, mContentRoot); } // done with the parser, pop it. context.popParser(); // set the AttachInfo on the root view. AttachInfo_Accessor.setAttachInfo(mViewRoot); // post-inflate process. For now this supports TabHost/TabWidget postInflateProcess(view, params.getLayoutlibCallback(), isPreference ? view : null); mInflater.onDoneInflation(); setActiveToolbar(view, context, params); measureLayout(params); measureView(mViewRoot, null /*measuredView*/, mMeasuredScreenWidth, MeasureSpec.EXACTLY, mMeasuredScreenHeight, MeasureSpec.EXACTLY); mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); mSystemViewInfoList = visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(), false); Choreographer_Delegate.clearFrames(); return SUCCESS.createResult(); } catch (PostInflateException e) { return ERROR_INFLATION.createResult(e.getMessage(), e); } catch (Throwable e) { // get the real cause of the exception. Throwable t = e; while (t.getCause() != null) { t = t.getCause(); } return ERROR_INFLATION.createResult(t.getMessage(), t); } } /** * Sets the time for which the next frame will be selected. The time is the elapsed time from * the current system nanos time. You */ public void setElapsedFrameTimeNanos(long nanos) { mElapsedFrameTimeNanos = nanos; } /** * Runs a layout pass for the given view root */ private static void doLayout(@NonNull BridgeContext context, @NonNull ViewGroup viewRoot, int width, int height) { // measure again with the size we need // This must always be done before the call to layout measureView(viewRoot, null /*measuredView*/, width, MeasureSpec.EXACTLY, height, MeasureSpec.EXACTLY); // now do the layout. viewRoot.layout(0, 0, width, height); handleScrolling(context, viewRoot); } /** * Renders the given view hierarchy to the passed canvas and returns the result of the render * operation. * @param canvas an optional canvas to render the views to. If null, only the measure and * layout steps will be executed. */ private static Result renderAndBuildResult(@NonNull ViewGroup viewRoot, @Nullable Canvas canvas) { if (canvas == null) { return SUCCESS.createResult(); } AttachInfo_Accessor.dispatchOnPreDraw(viewRoot); viewRoot.draw(canvas); return SUCCESS.createResult(); } /** * Renders the scene. *

* {@link #acquire(long)} must have been called before this. * * @param freshRender whether the render is a new one and should erase the existing bitmap (in * the case where bitmaps are reused). This is typically needed when not playing * animations.) * * @throws IllegalStateException if the current context is different than the one owned by * the scene, or if {@link #acquire(long)} was not called. * * @see SessionParams#getRenderingMode() * @see RenderSession#render(long) */ public Result render(boolean freshRender) { return renderAndBuildResult(freshRender, false); } /** * Measures the layout *

* {@link #acquire(long)} must have been called before this. * * @throws IllegalStateException if the current context is different than the one owned by * the scene, or if {@link #acquire(long)} was not called. * * @see SessionParams#getRenderingMode() * @see RenderSession#render(long) */ public Result measure() { return renderAndBuildResult(false, true); } /** * Renders the scene. *

* {@link #acquire(long)} must have been called before this. * * @param freshRender whether the render is a new one and should erase the existing bitmap (in * the case where bitmaps are reused). This is typically needed when not playing * animations.) * * @throws IllegalStateException if the current context is different than the one owned by * the scene, or if {@link #acquire(long)} was not called. * * @see SessionParams#getRenderingMode() * @see RenderSession#render(long) */ private Result renderAndBuildResult(boolean freshRender, boolean onlyMeasure) { checkLock(); SessionParams params = getParams(); try { if (mViewRoot == null) { return ERROR_NOT_INFLATED.createResult(); } measureLayout(params); HardwareConfig hardwareConfig = params.getHardwareConfig(); Result renderResult = SUCCESS.createResult(); if (onlyMeasure) { // delete the canvas and image to reset them on the next full rendering mImage = null; mCanvas = null; doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight); } else { // draw the views // create the BufferedImage into which the layout will be rendered. boolean newImage = false; // When disableBitmapCaching is true, we do not reuse mImage and // we create a new one in every render. // This is useful when mImage is just a wrapper of Graphics2D so // it doesn't get cached. boolean disableBitmapCaching = Boolean.TRUE.equals(params.getFlag( RenderParamsFlags.FLAG_KEY_DISABLE_BITMAP_CACHING)); if (mNewRenderSize || mCanvas == null || disableBitmapCaching) { mNewRenderSize = false; if (params.getImageFactory() != null) { mImage = params.getImageFactory().getImage( mMeasuredScreenWidth, mMeasuredScreenHeight); } else { mImage = new BufferedImage( mMeasuredScreenWidth, mMeasuredScreenHeight, BufferedImage.TYPE_INT_ARGB); newImage = true; } if (params.isTransparentBackground()) { // since we override the content, it's the same as if it was a new image. newImage = true; Graphics2D gc = mImage.createGraphics(); gc.setColor(new Color(0, true)); gc.setComposite(AlphaComposite.Src); gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); gc.dispose(); } // create an Android bitmap around the BufferedImage Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage, true /*isMutable*/, hardwareConfig.getDensity()); if (mCanvas == null) { // create a Canvas around the Android bitmap mCanvas = new Canvas(bitmap); } else { mCanvas.setBitmap(bitmap); } boolean enableImageResizing = mImage.getWidth() != mMeasuredScreenWidth && mImage.getHeight() != mMeasuredScreenHeight && Boolean.TRUE.equals(params.getFlag( RenderParamsFlags.FLAG_KEY_RESULT_IMAGE_AUTO_SCALE)); if (enableImageResizing) { float scaleX = (float)mImage.getWidth() / mMeasuredScreenWidth; float scaleY = (float)mImage.getHeight() / mMeasuredScreenHeight; mCanvas.scale(scaleX, scaleY); } mCanvas.setDensity(hardwareConfig.getDensity().getDpiValue()); } if (freshRender && !newImage) { Graphics2D gc = mImage.createGraphics(); gc.setComposite(AlphaComposite.Src); gc.setColor(new Color(0x00000000, true)); gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); // done gc.dispose(); } doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight); if (mElapsedFrameTimeNanos >= 0) { long initialTime = System_Delegate.nanoTime(); if (!mFirstFrameExecuted) { // We need to run an initial draw call to initialize the animations renderAndBuildResult(mViewRoot, NOP_CANVAS); // The first frame will initialize the animations Choreographer_Delegate.doFrame(initialTime); mFirstFrameExecuted = true; } // Second frame will move the animations Choreographer_Delegate.doFrame(initialTime + mElapsedFrameTimeNanos); } renderResult = renderAndBuildResult(mViewRoot, mCanvas); } mSystemViewInfoList = visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(), false); try { boolean enableLayoutValidation = Boolean.TRUE.equals(params.getFlag(RenderParamsFlags.FLAG_ENABLE_LAYOUT_VALIDATOR)); boolean enableLayoutValidationImageCheck = Boolean.TRUE.equals( params.getFlag(RenderParamsFlags.FLAG_ENABLE_LAYOUT_VALIDATOR_IMAGE_CHECK)); if (enableLayoutValidation && !getViewInfos().isEmpty()) { AccessibilityHierarchyAndroid_ViewElementClassNamesAndroid_Delegate.sLayoutlibCallback = getContext().getLayoutlibCallback(); BufferedImage imageToPass = enableLayoutValidationImageCheck ? getImage() : null; ValidatorResult validatorResult = LayoutValidator.validate(((View) getViewInfos().get(0).getViewObject()), imageToPass); setValidatorResult(validatorResult); } } catch (Throwable e) { ValidatorResult.Builder builder = new Builder(); builder.mMetric.mErrorMessage = e.getMessage(); setValidatorResult(builder.build()); } finally { AccessibilityHierarchyAndroid_ViewElementClassNamesAndroid_Delegate.sLayoutlibCallback = null; } // success! return renderResult; } catch (Throwable e) { // get the real cause of the exception. Throwable t = e; while (t.getCause() != null) { t = t.getCause(); } return ERROR_UNKNOWN.createResult(t.getMessage(), t); } } /** * Executes {@link View#measure(int, int)} on a given view with the given parameters (used * to create measure specs with {@link MeasureSpec#makeMeasureSpec(int, int)}. * * if measuredView is non null, the method returns a {@link Pair} of (width, height) * for the view (using {@link View#getMeasuredWidth()} and {@link View#getMeasuredHeight()}). * * @param viewToMeasure the view on which to execute measure(). * @param measuredView if non null, the view to query for its measured width/height. * @param width the width to use in the MeasureSpec. * @param widthMode the MeasureSpec mode to use for the width. * @param height the height to use in the MeasureSpec. * @param heightMode the MeasureSpec mode to use for the height. * @return the measured width/height if measuredView is non-null, null otherwise. */ private static Pair measureView(ViewGroup viewToMeasure, View measuredView, int width, int widthMode, int height, int heightMode) { int w_spec = MeasureSpec.makeMeasureSpec(width, widthMode); int h_spec = MeasureSpec.makeMeasureSpec(height, heightMode); viewToMeasure.measure(w_spec, h_spec); if (measuredView != null) { return Pair.of(measuredView.getMeasuredWidth(), measuredView.getMeasuredHeight()); } return null; } /** * Post process on a view hierarchy that was just inflated. *

* At the moment this only supports TabHost: If {@link TabHost} is detected, look for the * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically * based on the content of the {@link FrameLayout}. * @param view the root view to process. * @param layoutlibCallback callback to the project. * @param skip the view and it's children are not processed. */ private void postInflateProcess(View view, LayoutlibCallback layoutlibCallback, View skip) throws PostInflateException { if (view == skip) { return; } if (view instanceof TabHost) { setupTabHost((TabHost) view, layoutlibCallback); } else if (view instanceof QuickContactBadge) { QuickContactBadge badge = (QuickContactBadge) view; badge.setImageToDefault(); } else if (view instanceof AdapterView) { // get the view ID. int id = view.getId(); BridgeContext context = getContext(); // get a ResourceReference from the integer ID. ResourceReference listRef = context.resolveId(id); if (listRef != null) { SessionParams params = getParams(); AdapterBinding binding = params.getAdapterBindings().get(listRef); // if there was no adapter binding, trying to get it from the call back. if (binding == null) { binding = layoutlibCallback.getAdapterBinding( listRef, context.getViewKey(view), view); } if (binding != null) { if (view instanceof AbsListView) { if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) && view instanceof ListView) { ListView list = (ListView) view; boolean skipCallbackParser = false; int count = binding.getHeaderCount(); for (int i = 0; i < count; i++) { Pair pair = context.inflateView( binding.getHeaderAt(i), list, false, skipCallbackParser); if (pair.getFirst() != null) { list.addHeaderView(pair.getFirst()); } skipCallbackParser |= pair.getSecond(); } count = binding.getFooterCount(); for (int i = 0; i < count; i++) { Pair pair = context.inflateView( binding.getFooterAt(i), list, false, skipCallbackParser); if (pair.getFirst() != null) { list.addFooterView(pair.getFirst()); } skipCallbackParser |= pair.getSecond(); } } if (view instanceof ExpandableListView) { ((ExpandableListView) view).setAdapter( new FakeExpandableAdapter(listRef, binding, layoutlibCallback)); } else { ((AbsListView) view).setAdapter( new FakeAdapter(listRef, binding, layoutlibCallback)); } } else if (view instanceof AbsSpinner) { ((AbsSpinner) view).setAdapter( new FakeAdapter(listRef, binding, layoutlibCallback)); } } } } else if (view instanceof ViewGroup) { mInflater.postInflateProcess(view); ViewGroup group = (ViewGroup) view; final int count = group.getChildCount(); for (int c = 0; c < count; c++) { View child = group.getChildAt(c); postInflateProcess(child, layoutlibCallback, skip); } } } /** * If the root layout is a CoordinatorLayout with an AppBar: * Set the title of the AppBar to the title of the activity context. */ private void setActiveToolbar(View view, BridgeContext context, SessionParams params) { View coordinatorLayout = findChildView(view, DesignLibUtil.CN_COORDINATOR_LAYOUT); if (coordinatorLayout == null) { return; } View appBar = findChildView(coordinatorLayout, DesignLibUtil.CN_APPBAR_LAYOUT); if (appBar == null) { return; } ViewGroup collapsingToolbar = (ViewGroup) findChildView(appBar, DesignLibUtil.CN_COLLAPSING_TOOLBAR_LAYOUT); if (collapsingToolbar == null) { return; } if (!hasToolbar(collapsingToolbar)) { return; } String title = params.getAppLabel(); DesignLibUtil.setTitle(collapsingToolbar, title); } private View findChildView(View view, String[] className) { if (!(view instanceof ViewGroup)) { return null; } ViewGroup group = (ViewGroup) view; for (int i = 0; i < group.getChildCount(); i++) { if (isInstanceOf(group.getChildAt(i), className)) { return group.getChildAt(i); } } return null; } private boolean hasToolbar(View collapsingToolbar) { if (!(collapsingToolbar instanceof ViewGroup)) { return false; } ViewGroup group = (ViewGroup) collapsingToolbar; for (int i = 0; i < group.getChildCount(); i++) { if (isInstanceOf(group.getChildAt(i), DesignLibUtil.CN_TOOLBAR)) { return true; } } return false; } /** * Set the scroll position on all the components with the "scrollX" and "scrollY" attribute. If * the component supports nested scrolling attempt that first, then use the unconsumed scroll * part to scroll the content in the component. */ private static void handleScrolling(BridgeContext context, View view) { int scrollPosX = context.getScrollXPos(view); int scrollPosY = context.getScrollYPos(view); if (scrollPosX != 0 || scrollPosY != 0) { if (view.isNestedScrollingEnabled()) { int[] consumed = new int[2]; int axis = scrollPosX != 0 ? View.SCROLL_AXIS_HORIZONTAL : 0; axis |= scrollPosY != 0 ? View.SCROLL_AXIS_VERTICAL : 0; if (view.startNestedScroll(axis)) { view.dispatchNestedPreScroll(scrollPosX, scrollPosY, consumed, null); view.dispatchNestedScroll(consumed[0], consumed[1], scrollPosX, scrollPosY, null); view.stopNestedScroll(); scrollPosX -= consumed[0]; scrollPosY -= consumed[1]; } } if (scrollPosX != 0 || scrollPosY != 0) { view.scrollTo(scrollPosX, scrollPosY); } } if (!(view instanceof ViewGroup)) { return; } ViewGroup group = (ViewGroup) view; for (int i = 0; i < group.getChildCount(); i++) { View child = group.getChildAt(i); handleScrolling(context, child); } } /** * Sets up a {@link TabHost} object. * @param tabHost the TabHost to setup. * @param layoutlibCallback The project callback object to access the project R class. * @throws PostInflateException if TabHost is missing the required ids for TabHost */ private void setupTabHost(TabHost tabHost, LayoutlibCallback layoutlibCallback) throws PostInflateException { // look for the TabWidget, and the FrameLayout. They have their own specific names View v = tabHost.findViewById(android.R.id.tabs); if (v == null) { throw new PostInflateException( "TabHost requires a TabWidget with id \"android:id/tabs\".\n"); } if (!(v instanceof TabWidget)) { throw new PostInflateException(String.format( "TabHost requires a TabWidget with id \"android:id/tabs\".\n" + "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName())); } v = tabHost.findViewById(android.R.id.tabcontent); if (v == null) { // TODO: see if we can fake tabs even without the FrameLayout (same below when the frameLayout is empty) //noinspection SpellCheckingInspection throw new PostInflateException( "TabHost requires a FrameLayout with id \"android:id/tabcontent\"."); } if (!(v instanceof FrameLayout)) { //noinspection SpellCheckingInspection throw new PostInflateException(String.format( "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" + "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName())); } FrameLayout content = (FrameLayout)v; // now process the content of the frameLayout and dynamically create tabs for it. final int count = content.getChildCount(); // this must be called before addTab() so that the TabHost searches its TabWidget // and FrameLayout. if (isInstanceOf(tabHost, FragmentTabHostUtil.CN_FRAGMENT_TAB_HOST)) { FragmentTabHostUtil.setup(tabHost, getContext()); } else { tabHost.setup(); } if (count == 0) { // Create a placeholder child to get a single tab TabSpec spec = tabHost.newTabSpec("tag") .setIndicator("Tab Label", tabHost.getResources() .getDrawable(android.R.drawable.ic_menu_info_details, null)) .setContent(tag -> new LinearLayout(getContext())); tabHost.addTab(spec); } else { // for each child of the frameLayout, add a new TabSpec for (int i = 0 ; i < count ; i++) { View child = content.getChildAt(i); String tabSpec = String.format("tab_spec%d", i+1); int id = child.getId(); ResourceReference resource = layoutlibCallback.resolveResourceId(id); String name; if (resource != null) { name = resource.getName(); } else { name = String.format("Tab %d", i+1); // default name if id is unresolved. } tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id)); } } } /** * Visits a {@link View} and its children and generate a {@link ViewInfo} containing the * bounds of all the views. * * @param view the root View * @param hOffset horizontal offset for the view bounds. * @param vOffset vertical offset for the view bounds. * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object. * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the * content frame. * * @return {@code ViewInfo} containing the bounds of the view and it children otherwise. */ private ViewInfo visit(View view, int hOffset, int vOffset, boolean setExtendedInfo, boolean isContentFrame) { ViewInfo result = createViewInfo(view, hOffset, vOffset, setExtendedInfo, isContentFrame); if (view instanceof ViewGroup) { ViewGroup group = ((ViewGroup) view); result.setChildren(visitAllChildren(group, isContentFrame ? 0 : hOffset, isContentFrame ? 0 : vOffset, setExtendedInfo, isContentFrame)); } return result; } /** * Visits all the children of a given ViewGroup and generates a list of {@link ViewInfo} * containing the bounds of all the views. It also initializes the {@link #mViewInfoList} with * the children of the {@code mContentRoot}. * * @param viewGroup the root View * @param hOffset horizontal offset from the top for the content view frame. * @param vOffset vertical offset from the top for the content view frame. * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object. * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the * content frame. {@code false} if the {@code ViewInfo} to be created is * part of the system decor. */ private List visitAllChildren(ViewGroup viewGroup, int hOffset, int vOffset, boolean setExtendedInfo, boolean isContentFrame) { if (viewGroup == null) { return null; } if (!isContentFrame) { vOffset += viewGroup.getTop(); hOffset += viewGroup.getLeft(); } int childCount = viewGroup.getChildCount(); if (viewGroup == mContentRoot) { List childrenWithoutOffset = new ArrayList<>(childCount); List childrenWithOffset = new ArrayList<>(childCount); for (int i = 0; i < childCount; i++) { ViewInfo[] childViewInfo = visitContentRoot(viewGroup.getChildAt(i), hOffset, vOffset, setExtendedInfo); childrenWithoutOffset.add(childViewInfo[0]); childrenWithOffset.add(childViewInfo[1]); } mViewInfoList = childrenWithOffset; return childrenWithoutOffset; } else { List children = new ArrayList<>(childCount); for (int i = 0; i < childCount; i++) { children.add(visit(viewGroup.getChildAt(i), hOffset, vOffset, setExtendedInfo, isContentFrame)); } return children; } } /** * Visits the children of {@link #mContentRoot} and generates {@link ViewInfo} containing the * bounds of all the views. It returns two {@code ViewInfo} objects with the same children, * one with the {@code offset} and other without the {@code offset}. The offset is needed to * get the right bounds if the {@code ViewInfo} hierarchy is accessed from * {@code mViewInfoList}. When the hierarchy is accessed via {@code mSystemViewInfoList}, the * offset is not needed. * * @return an array of length two, with ViewInfo at index 0 is without offset and ViewInfo at * index 1 is with the offset. */ @NonNull private ViewInfo[] visitContentRoot(View view, int hOffset, int vOffset, boolean setExtendedInfo) { ViewInfo[] result = new ViewInfo[2]; if (view == null) { return result; } result[0] = createViewInfo(view, 0, 0, setExtendedInfo, true); result[1] = createViewInfo(view, hOffset, vOffset, setExtendedInfo, true); if (view instanceof ViewGroup) { List children = visitAllChildren((ViewGroup) view, 0, 0, setExtendedInfo, true); result[0].setChildren(children); result[1].setChildren(children); } return result; } /** * Creates a {@link ViewInfo} for the view. The {@code ViewInfo} corresponding to the children * of the {@code view} are not created. Consequently, the children of {@code ViewInfo} is not * set. * @param hOffset horizontal offset for the view bounds. Used only if view is part of the * content frame. * @param vOffset vertial an offset for the view bounds. Used only if view is part of the * content frame. */ private ViewInfo createViewInfo(View view, int hOffset, int vOffset, boolean setExtendedInfo, boolean isContentFrame) { if (view == null) { return null; } ViewParent parent = view.getParent(); ViewInfo result; if (isContentFrame) { // Account for parent scroll values when calculating the bounding box int scrollX = parent != null ? ((View)parent).getScrollX() : 0; int scrollY = parent != null ? ((View)parent).getScrollY() : 0; // The view is part of the layout added by the user. Hence, // the ViewCookie may be obtained only through the Context. int shiftX = -scrollX + Math.round(view.getTranslationX()) + hOffset; int shiftY = -scrollY + Math.round(view.getTranslationY()) + vOffset; result = new ViewInfo(view.getClass().getName(), getContext().getViewKey(view), shiftX + view.getLeft(), shiftY + view.getTop(), shiftX + view.getRight(), shiftY + view.getBottom(), view, view.getLayoutParams()); } else { // We are part of the system decor. SystemViewInfo r = new SystemViewInfo(view.getClass().getName(), getViewKey(view), view.getLeft(), view.getTop(), view.getRight(), view.getBottom(), view, view.getLayoutParams()); result = r; // We currently mark three kinds of views: // 1. Menus in the Action Bar // 2. Menus in the Overflow popup. // 3. The overflow popup button. if (view instanceof ListMenuItemView) { // Mark 2. // All menus in the popup are of type ListMenuItemView. r.setViewType(ViewType.ACTION_BAR_OVERFLOW_MENU); } else { // Mark 3. ViewGroup.LayoutParams lp = view.getLayoutParams(); if (lp instanceof ActionMenuView.LayoutParams && ((ActionMenuView.LayoutParams) lp).isOverflowButton) { r.setViewType(ViewType.ACTION_BAR_OVERFLOW); } else { // Mark 1. // A view is a menu in the Action Bar is it is not the overflow button and of // its parent is of type ActionMenuView. We can also check if the view is // instanceof ActionMenuItemView but that will fail for menus using // actionProviderClass. while (parent != mViewRoot && parent instanceof ViewGroup) { if (parent instanceof ActionMenuView) { r.setViewType(ViewType.ACTION_BAR_MENU); break; } parent = parent.getParent(); } } } } if (setExtendedInfo) { MarginLayoutParams marginParams = null; LayoutParams params = view.getLayoutParams(); if (params instanceof MarginLayoutParams) { marginParams = (MarginLayoutParams) params; } result.setExtendedInfo(view.getBaseline(), marginParams != null ? marginParams.leftMargin : 0, marginParams != null ? marginParams.topMargin : 0, marginParams != null ? marginParams.rightMargin : 0, marginParams != null ? marginParams.bottomMargin : 0); } return result; } /* (non-Javadoc) * The cookie for menu items are stored in menu item and not in the map from View stored in * BridgeContext. */ @Nullable private Object getViewKey(View view) { BridgeContext context = getContext(); if (!(view instanceof MenuView.ItemView)) { return context.getViewKey(view); } MenuItemImpl menuItem; if (view instanceof ActionMenuItemView) { menuItem = ((ActionMenuItemView) view).getItemData(); } else if (view instanceof ListMenuItemView) { menuItem = ((ListMenuItemView) view).getItemData(); } else if (view instanceof IconMenuItemView) { menuItem = ((IconMenuItemView) view).getItemData(); } else { menuItem = null; } if (menuItem instanceof BridgeMenuItemImpl) { return ((BridgeMenuItemImpl) menuItem).getViewCookie(); } return null; } public void invalidateRenderingSize() { mMeasuredScreenWidth = mMeasuredScreenHeight = -1; } public BufferedImage getImage() { return mImage; } public List getViewInfos() { return mViewInfoList; } public List getSystemViewInfos() { return mSystemViewInfoList; } public Map> getDefaultNamespacedProperties() { return getContext().getDefaultProperties(); } public Map getDefaultStyles() { Map defaultStyles = new IdentityHashMap<>(); Map namespacedStyles = getDefaultNamespacedStyles(); for (Object key : namespacedStyles.keySet()) { ResourceReference style = namespacedStyles.get(key); defaultStyles.put(key, style.getQualifiedName()); } return defaultStyles; } public Map getDefaultNamespacedStyles() { return getContext().getDefaultNamespacedStyles(); } @Nullable public ValidatorResult getValidatorResult() { return mValidatorResult; } public void setValidatorResult(ValidatorResult result) { mValidatorResult = result; } public void setScene(RenderSession session) { mScene = session; } public RenderSession getSession() { return mScene; } public void dispose() { try { boolean createdLooper = false; if (Looper.myLooper() == null) { // Detaching the root view from the window will try to stop any running animations. // The stop method checks that it can run in the looper so, if there is no current // looper, we create a temporary one to complete the shutdown. Bridge.prepareThread(); createdLooper = true; } AttachInfo_Accessor.detachFromWindow(mViewRoot); if (mCanvas != null) { mCanvas.release(); mCanvas = null; } if (mViewInfoList != null) { mViewInfoList.clear(); } if (mSystemViewInfoList != null) { mSystemViewInfoList.clear(); } mImage = null; mViewRoot = null; mContentRoot = null; NinePatch_Delegate.clearCache(); if (createdLooper) { Choreographer_Delegate.dispose(); Bridge.cleanupThread(); } } catch (Throwable t) { getContext().error("Error while disposing a RenderSession", t); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy