| /* |
| * 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.IAnimationListener; |
| import com.android.ide.common.rendering.api.ILayoutPullParser; |
| import com.android.ide.common.rendering.api.LayoutlibCallback; |
| import com.android.ide.common.rendering.api.RenderResources; |
| 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.Result.Status; |
| import com.android.ide.common.rendering.api.SessionParams; |
| import com.android.ide.common.rendering.api.SessionParams.RenderingMode; |
| import com.android.ide.common.rendering.api.StyleResourceValue; |
| import com.android.ide.common.rendering.api.ViewInfo; |
| import com.android.ide.common.rendering.api.ViewType; |
| import com.android.internal.util.XmlUtils; |
| 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.BridgeLayoutParamsMapAttributes; |
| import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; |
| import com.android.layoutlib.bridge.android.RenderParamsFlags; |
| import com.android.layoutlib.bridge.android.support.DesignLibUtil; |
| import com.android.layoutlib.bridge.android.support.RecyclerViewUtil; |
| import com.android.layoutlib.bridge.bars.AppCompatActionBar; |
| import com.android.layoutlib.bridge.bars.BridgeActionBar; |
| import com.android.layoutlib.bridge.bars.Config; |
| import com.android.layoutlib.bridge.bars.FrameworkActionBar; |
| import com.android.layoutlib.bridge.bars.NavigationBar; |
| import com.android.layoutlib.bridge.bars.StatusBar; |
| import com.android.layoutlib.bridge.bars.TitleBar; |
| import com.android.layoutlib.bridge.impl.binding.FakeAdapter; |
| import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter; |
| import com.android.resources.Density; |
| import com.android.resources.ResourceType; |
| import com.android.resources.ScreenOrientation; |
| import com.android.util.Pair; |
| |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import android.animation.AnimationThread; |
| import android.animation.Animator; |
| import android.animation.AnimatorInflater; |
| import android.animation.LayoutTransition; |
| import android.animation.LayoutTransition.TransitionListener; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.Fragment_Delegate; |
| import android.graphics.Bitmap; |
| import android.graphics.Bitmap_Delegate; |
| import android.graphics.Canvas; |
| import android.graphics.drawable.Drawable; |
| import android.preference.Preference_Delegate; |
| import android.util.DisplayMetrics; |
| import android.util.TypedValue; |
| import android.view.AttachInfo_Accessor; |
| import android.view.BridgeInflater; |
| import android.view.IWindowManager; |
| import android.view.IWindowManagerImpl; |
| import android.view.Surface; |
| 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.view.WindowManagerGlobal_Delegate; |
| 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.List; |
| import java.util.Map; |
| |
| import static com.android.ide.common.rendering.api.Result.Status.ERROR_ANIM_NOT_FOUND; |
| 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.ERROR_VIEWGROUP_NO_CHILDREN; |
| import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; |
| |
| /** |
| * Class implementing the render session. |
| * <p/> |
| * 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<SessionParams> { |
| |
| private static final int DEFAULT_TITLE_BAR_HEIGHT = 25; |
| private static final int DEFAULT_STATUS_BAR_HEIGHT = 25; |
| |
| // scene state |
| private RenderSession mScene; |
| private BridgeXmlBlockParser mBlockParser; |
| private BridgeInflater mInflater; |
| private ResourceValue mWindowBackground; |
| private ViewGroup mViewRoot; |
| private FrameLayout mContentRoot; |
| private Canvas mCanvas; |
| private int mMeasuredScreenWidth = -1; |
| private int mMeasuredScreenHeight = -1; |
| private boolean mIsAlphaChannelImage; |
| private boolean mWindowIsFloating; |
| private Boolean mIsThemeAppCompat; |
| |
| private int mStatusBarSize; |
| private int mNavigationBarSize; |
| private int mNavigationBarOrientation = LinearLayout.HORIZONTAL; |
| private int mTitleBarSize; |
| private int mActionBarSize; |
| |
| // information being returned through the API |
| private BufferedImage mImage; |
| private List<ViewInfo> mViewInfoList; |
| private List<ViewInfo> mSystemViewInfoList; |
| |
| private static final class PostInflateException extends Exception { |
| private static final long serialVersionUID = 1L; |
| |
| public PostInflateException(String message) { |
| super(message); |
| } |
| } |
| |
| /** |
| * Creates a layout scene with all the information coming from the layout bridge API. |
| * <p> |
| * This <b>must</b> 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(); |
| |
| |
| RenderResources resources = getParams().getResources(); |
| DisplayMetrics metrics = getContext().getMetrics(); |
| |
| // use default of true in case it's not found to use alpha by default |
| mIsAlphaChannelImage = getBooleanThemeValue(resources, "windowIsFloating", true, true); |
| // FIXME: Find out why both variables are taking the same value. |
| mWindowIsFloating = getBooleanThemeValue(resources, "windowIsFloating", true, true); |
| |
| findBackground(resources); |
| findStatusBar(resources, metrics); |
| findActionBar(resources, metrics); |
| findNavigationBar(resources, metrics); |
| |
| // FIXME: find those out, and possibly add them to the render params |
| boolean hasNavigationBar = true; |
| //noinspection ConstantConditions |
| IWindowManager iwm = new IWindowManagerImpl(getContext().getConfiguration(), |
| metrics, Surface.ROTATION_0, |
| hasNavigationBar); |
| WindowManagerGlobal_Delegate.setWindowManagerService(iwm); |
| |
| // build the inflater and parser. |
| mInflater = new BridgeInflater(context, params.getLayoutlibCallback()); |
| context.setBridgeInflater(mInflater); |
| |
| mBlockParser = new BridgeXmlBlockParser( |
| params.getLayoutDescription(), context, false /* platformResourceFlag */); |
| |
| return SUCCESS.createResult(); |
| } |
| |
| /** |
| * Inflates the layout. |
| * <p> |
| * {@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 { |
| |
| SessionParams params = getParams(); |
| HardwareConfig hardwareConfig = params.getHardwareConfig(); |
| BridgeContext context = getContext(); |
| boolean isRtl = Bridge.isLocaleRtl(params.getLocale()); |
| int layoutDirection = isRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR; |
| |
| // the view group that receives the window background. |
| ViewGroup backgroundView; |
| |
| if (mWindowIsFloating || params.isForceNoDecor()) { |
| backgroundView = mViewRoot = mContentRoot = new FrameLayout(context); |
| mViewRoot.setLayoutDirection(layoutDirection); |
| } else { |
| int simulatedPlatformVersion = params.getSimulatedPlatformVersion(); |
| if (hasSoftwareButtons() && mNavigationBarOrientation == LinearLayout.VERTICAL) { |
| /* |
| * This is a special case where the navigation bar is on the right. |
| +-------------------------------------------------+---+ |
| | Status bar (always) | | |
| +-------------------------------------------------+ | |
| | (Layout with background drawable) | | |
| | +---------------------------------------------+ | | |
| | | Title/Action bar (optional) | | | |
| | +---------------------------------------------+ | | |
| | | Content, vertical extending | | | |
| | | | | | |
| | +---------------------------------------------+ | | |
| +-------------------------------------------------+---+ |
| |
| So we create a horizontal layout, with the nav bar on the right, |
| and the left part is the normal layout below without the nav bar at |
| the bottom |
| */ |
| LinearLayout topLayout = new LinearLayout(context); |
| topLayout.setLayoutDirection(layoutDirection); |
| mViewRoot = topLayout; |
| topLayout.setOrientation(LinearLayout.HORIZONTAL); |
| |
| if (Config.showOnScreenNavBar(simulatedPlatformVersion)) { |
| try { |
| NavigationBar navigationBar = createNavigationBar(context, |
| hardwareConfig.getDensity(), isRtl, params.isRtlSupported(), |
| simulatedPlatformVersion); |
| topLayout.addView(navigationBar); |
| } catch (XmlPullParserException ignored) { |
| } |
| } |
| } |
| |
| /* |
| * we're creating the following layout |
| * |
| +-------------------------------------------------+ |
| | Status bar (always) | |
| +-------------------------------------------------+ |
| | (Layout with background drawable) | |
| | +---------------------------------------------+ | |
| | | Title/Action bar (optional) | | |
| | +---------------------------------------------+ | |
| | | Content, vertical extending | | |
| | | | | |
| | +---------------------------------------------+ | |
| +-------------------------------------------------+ |
| | Navigation bar for soft buttons, maybe see above| |
| +-------------------------------------------------+ |
| |
| */ |
| |
| LinearLayout topLayout = new LinearLayout(context); |
| topLayout.setOrientation(LinearLayout.VERTICAL); |
| topLayout.setLayoutDirection(layoutDirection); |
| // if we don't already have a view root this is it |
| if (mViewRoot == null) { |
| mViewRoot = topLayout; |
| } else { |
| int topLayoutWidth = |
| params.getHardwareConfig().getScreenWidth() - mNavigationBarSize; |
| LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( |
| topLayoutWidth, LayoutParams.MATCH_PARENT); |
| topLayout.setLayoutParams(layoutParams); |
| |
| // this is the case of soft buttons + vertical bar. |
| // this top layout is the first layout in the horizontal layout. see above) |
| if (isRtl && params.isRtlSupported()) { |
| // If RTL is enabled, layoutlib will mirror the layouts. So, add the |
| // topLayout to the right of Navigation Bar and layoutlib will draw it |
| // to the left. |
| mViewRoot.addView(topLayout); |
| } else { |
| // Add the top layout to the left of the Navigation Bar. |
| mViewRoot.addView(topLayout, 0); |
| } |
| } |
| |
| if (mStatusBarSize > 0) { |
| // system bar |
| try { |
| StatusBar statusBar = createStatusBar(context, hardwareConfig.getDensity(), |
| layoutDirection, params.isRtlSupported(), |
| simulatedPlatformVersion); |
| topLayout.addView(statusBar); |
| } catch (XmlPullParserException ignored) { |
| |
| } |
| } |
| |
| LinearLayout backgroundLayout = new LinearLayout(context); |
| backgroundView = backgroundLayout; |
| backgroundLayout.setOrientation(LinearLayout.VERTICAL); |
| LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( |
| LayoutParams.MATCH_PARENT, 0); |
| layoutParams.weight = 1; |
| backgroundLayout.setLayoutParams(layoutParams); |
| topLayout.addView(backgroundLayout); |
| |
| |
| // if the theme says no title/action bar, then the size will be 0 |
| if (mActionBarSize > 0) { |
| BridgeActionBar actionBar = createActionBar(context, params, backgroundLayout); |
| actionBar.createMenuPopup(); |
| mContentRoot = actionBar.getContentRoot(); |
| } else if (mTitleBarSize > 0) { |
| try { |
| TitleBar titleBar = createTitleBar(context, |
| params.getAppLabel(), |
| simulatedPlatformVersion); |
| backgroundLayout.addView(titleBar); |
| } catch (XmlPullParserException ignored) { |
| |
| } |
| } |
| |
| // content frame |
| if (mContentRoot == null) { |
| mContentRoot = new FrameLayout(context); |
| layoutParams = new LinearLayout.LayoutParams( |
| LayoutParams.MATCH_PARENT, 0); |
| layoutParams.weight = 1; |
| mContentRoot.setLayoutParams(layoutParams); |
| backgroundLayout.addView(mContentRoot); |
| } |
| |
| if (Config.showOnScreenNavBar(simulatedPlatformVersion) && |
| mNavigationBarOrientation == LinearLayout.HORIZONTAL && |
| mNavigationBarSize > 0) { |
| // system bar |
| try { |
| NavigationBar navigationBar = createNavigationBar(context, |
| hardwareConfig.getDensity(), isRtl, params.isRtlSupported(), |
| simulatedPlatformVersion); |
| topLayout.addView(navigationBar); |
| } catch (XmlPullParserException ignored) { |
| |
| } |
| } |
| } |
| |
| |
| // Sets the project callback (custom view loader) to the fragment delegate so that |
| // it can instantiate the custom Fragment. |
| Fragment_Delegate.setLayoutlibCallback(params.getLayoutlibCallback()); |
| |
| String rootTag = params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG); |
| boolean isPreference = "PreferenceScreen".equals(rootTag); |
| View view; |
| if (isPreference) { |
| view = Preference_Delegate.inflatePreference(getContext(), mBlockParser, |
| mContentRoot); |
| } else { |
| view = mInflater.inflate(mBlockParser, mContentRoot); |
| } |
| |
| // done with the parser, pop it. |
| context.popParser(); |
| |
| Fragment_Delegate.setLayoutlibCallback(null); |
| |
| // 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); |
| |
| setActiveToolbar(view, context, params); |
| |
| // get the background drawable |
| if (mWindowBackground != null) { |
| Drawable d = ResourceHelper.getDrawable(mWindowBackground, context); |
| backgroundView.setBackground(d); |
| } |
| |
| 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); |
| } |
| } |
| |
| /** |
| * Renders the scene. |
| * <p> |
| * {@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) { |
| checkLock(); |
| |
| SessionParams params = getParams(); |
| |
| try { |
| if (mViewRoot == null) { |
| return ERROR_NOT_INFLATED.createResult(); |
| } |
| |
| RenderingMode renderingMode = params.getRenderingMode(); |
| HardwareConfig hardwareConfig = params.getHardwareConfig(); |
| |
| // only do the screen measure when needed. |
| boolean newRenderSize = false; |
| if (mMeasuredScreenWidth == -1) { |
| newRenderSize = true; |
| mMeasuredScreenWidth = hardwareConfig.getScreenWidth(); |
| mMeasuredScreenHeight = hardwareConfig.getScreenHeight(); |
| |
| if (renderingMode != RenderingMode.NORMAL) { |
| int widthMeasureSpecMode = renderingMode.isHorizExpand() ? |
| MeasureSpec.UNSPECIFIED // this lets us know the actual needed size |
| : MeasureSpec.EXACTLY; |
| int heightMeasureSpecMode = renderingMode.isVertExpand() ? |
| 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. |
| |
| // first measure the full layout, with EXACTLY to get the size of the |
| // content as it is inside the decor/dialog |
| @SuppressWarnings("deprecation") |
| Pair<Integer, Integer> exactMeasure = measureView( |
| mViewRoot, mContentRoot.getChildAt(0), |
| 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. |
| @SuppressWarnings("deprecation") |
| Pair<Integer, Integer> result = measureView( |
| mContentRoot, mContentRoot.getChildAt(0), |
| mMeasuredScreenWidth, widthMeasureSpecMode, |
| mMeasuredScreenHeight, heightMeasureSpecMode); |
| |
| // now look at the difference and add what is needed. |
| if (renderingMode.isHorizExpand()) { |
| int measuredWidth = exactMeasure.getFirst(); |
| int neededWidth = result.getFirst(); |
| if (neededWidth > measuredWidth) { |
| mMeasuredScreenWidth += neededWidth - measuredWidth; |
| } |
| } |
| |
| if (renderingMode.isVertExpand()) { |
| int measuredHeight = exactMeasure.getSecond(); |
| int neededHeight = result.getSecond(); |
| if (neededHeight > measuredHeight) { |
| mMeasuredScreenHeight += neededHeight - measuredHeight; |
| } |
| } |
| } |
| } |
| |
| // measure again with the size we need |
| // This must always be done before the call to layout |
| measureView(mViewRoot, null /*measuredView*/, |
| mMeasuredScreenWidth, MeasureSpec.EXACTLY, |
| mMeasuredScreenHeight, MeasureSpec.EXACTLY); |
| |
| // now do the layout. |
| mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); |
| |
| handleScrolling(mViewRoot); |
| |
| if (params.isLayoutOnly()) { |
| // delete the canvas and image to reset them on the next full rendering |
| mImage = null; |
| mCanvas = null; |
| } else { |
| AttachInfo_Accessor.dispatchOnPreDraw(mViewRoot); |
| |
| // 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 (newRenderSize || mCanvas == null || disableBitmapCaching) { |
| if (params.getImageFactory() != null) { |
| mImage = params.getImageFactory().getImage( |
| mMeasuredScreenWidth, |
| mMeasuredScreenHeight); |
| } else { |
| mImage = new BufferedImage( |
| mMeasuredScreenWidth, |
| mMeasuredScreenHeight, |
| BufferedImage.TYPE_INT_ARGB); |
| newImage = true; |
| } |
| |
| if (params.isBgColorOverridden()) { |
| // 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(params.getOverrideBgColor(), 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); |
| } |
| 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(); |
| } |
| |
| mViewRoot.draw(mCanvas); |
| } |
| |
| mSystemViewInfoList = visitAllChildren(mViewRoot, 0, params.getExtendedViewInfoMode(), |
| false); |
| |
| // success! |
| return SUCCESS.createResult(); |
| } 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 <var>measuredView</var> 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. |
| */ |
| @SuppressWarnings("deprecation") // For the use of Pair |
| private Pair<Integer, Integer> 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; |
| } |
| |
| /** |
| * Animate an object |
| * <p> |
| * {@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 RenderSession#animate(Object, String, boolean, IAnimationListener) |
| */ |
| public Result animate(Object targetObject, String animationName, |
| boolean isFrameworkAnimation, IAnimationListener listener) { |
| checkLock(); |
| |
| BridgeContext context = getContext(); |
| |
| // find the animation file. |
| ResourceValue animationResource; |
| int animationId = 0; |
| if (isFrameworkAnimation) { |
| animationResource = context.getRenderResources().getFrameworkResource( |
| ResourceType.ANIMATOR, animationName); |
| if (animationResource != null) { |
| animationId = Bridge.getResourceId(ResourceType.ANIMATOR, animationName); |
| } |
| } else { |
| animationResource = context.getRenderResources().getProjectResource( |
| ResourceType.ANIMATOR, animationName); |
| if (animationResource != null) { |
| animationId = context.getLayoutlibCallback().getResourceId( |
| ResourceType.ANIMATOR, animationName); |
| } |
| } |
| |
| if (animationResource != null) { |
| try { |
| Animator anim = AnimatorInflater.loadAnimator(context, animationId); |
| if (anim != null) { |
| anim.setTarget(targetObject); |
| |
| new PlayAnimationThread(anim, this, animationName, listener).start(); |
| |
| return SUCCESS.createResult(); |
| } |
| } catch (Exception 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); |
| } |
| } |
| |
| return ERROR_ANIM_NOT_FOUND.createResult(); |
| } |
| |
| /** |
| * Insert a new child into an existing parent. |
| * <p> |
| * {@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 RenderSession#insertChild(Object, ILayoutPullParser, int, IAnimationListener) |
| */ |
| public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml, |
| final int index, IAnimationListener listener) { |
| checkLock(); |
| |
| BridgeContext context = getContext(); |
| |
| // create a block parser for the XML |
| BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( |
| childXml, context, false /* platformResourceFlag */); |
| |
| // inflate the child without adding it to the root since we want to control where it'll |
| // get added. We do pass the parentView however to ensure that the layoutParams will |
| // be created correctly. |
| final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/); |
| blockParser.ensurePopped(); |
| |
| invalidateRenderingSize(); |
| |
| if (listener != null) { |
| new AnimationThread(this, "insertChild", listener) { |
| |
| @Override |
| public Result preAnimation() { |
| parentView.setLayoutTransition(new LayoutTransition()); |
| return addView(parentView, child, index); |
| } |
| |
| @Override |
| public void postAnimation() { |
| parentView.setLayoutTransition(null); |
| } |
| }.start(); |
| |
| // always return success since the real status will come through the listener. |
| return SUCCESS.createResult(child); |
| } |
| |
| // add it to the parentView in the correct location |
| Result result = addView(parentView, child, index); |
| if (!result.isSuccess()) { |
| return result; |
| } |
| |
| result = render(false /*freshRender*/); |
| if (result.isSuccess()) { |
| result = result.getCopyWithData(child); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Adds a given view to a given parent at a given index. |
| * |
| * @param parent the parent to receive the view |
| * @param view the view to add to the parent |
| * @param index the index where to do the add. |
| * |
| * @return a Result with {@link Status#SUCCESS} or |
| * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support |
| * adding views. |
| */ |
| private Result addView(ViewGroup parent, View view, int index) { |
| try { |
| parent.addView(view, index); |
| return SUCCESS.createResult(); |
| } catch (UnsupportedOperationException e) { |
| // looks like this is a view class that doesn't support children manipulation! |
| return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); |
| } |
| } |
| |
| /** |
| * Moves a view to a new parent at a given location |
| * <p> |
| * {@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 RenderSession#moveChild(Object, Object, int, Map, IAnimationListener) |
| */ |
| public Result moveChild(final ViewGroup newParentView, final View childView, final int index, |
| Map<String, String> layoutParamsMap, final IAnimationListener listener) { |
| checkLock(); |
| |
| invalidateRenderingSize(); |
| |
| LayoutParams layoutParams = null; |
| if (layoutParamsMap != null) { |
| // need to create a new LayoutParams object for the new parent. |
| layoutParams = newParentView.generateLayoutParams( |
| new BridgeLayoutParamsMapAttributes(layoutParamsMap)); |
| } |
| |
| // get the current parent of the view that needs to be moved. |
| final ViewGroup previousParent = (ViewGroup) childView.getParent(); |
| |
| if (listener != null) { |
| final LayoutParams params = layoutParams; |
| |
| // there is no support for animating views across layouts, so in case the new and old |
| // parent views are different we fake the animation through a no animation thread. |
| if (previousParent != newParentView) { |
| new Thread("not animated moveChild") { |
| @Override |
| public void run() { |
| Result result = moveView(previousParent, newParentView, childView, index, |
| params); |
| if (!result.isSuccess()) { |
| listener.done(result); |
| } |
| |
| // ready to do the work, acquire the scene. |
| result = acquire(250); |
| if (!result.isSuccess()) { |
| listener.done(result); |
| return; |
| } |
| |
| try { |
| result = render(false /*freshRender*/); |
| if (result.isSuccess()) { |
| listener.onNewFrame(RenderSessionImpl.this.getSession()); |
| } |
| } finally { |
| release(); |
| } |
| |
| listener.done(result); |
| } |
| }.start(); |
| } else { |
| new AnimationThread(this, "moveChild", listener) { |
| |
| @Override |
| public Result preAnimation() { |
| // set up the transition for the parent. |
| LayoutTransition transition = new LayoutTransition(); |
| previousParent.setLayoutTransition(transition); |
| |
| // tweak the animation durations and start delays (to match the duration of |
| // animation playing just before). |
| // Note: Cannot user Animation.setDuration() directly. Have to set it |
| // on the LayoutTransition. |
| transition.setDuration(LayoutTransition.DISAPPEARING, 100); |
| // CHANGE_DISAPPEARING plays after DISAPPEARING |
| transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 100); |
| |
| transition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 100); |
| |
| transition.setDuration(LayoutTransition.CHANGE_APPEARING, 100); |
| // CHANGE_APPEARING plays after CHANGE_APPEARING |
| transition.setStartDelay(LayoutTransition.APPEARING, 100); |
| |
| transition.setDuration(LayoutTransition.APPEARING, 100); |
| |
| return moveView(previousParent, newParentView, childView, index, params); |
| } |
| |
| @Override |
| public void postAnimation() { |
| previousParent.setLayoutTransition(null); |
| newParentView.setLayoutTransition(null); |
| } |
| }.start(); |
| } |
| |
| // always return success since the real status will come through the listener. |
| return SUCCESS.createResult(layoutParams); |
| } |
| |
| Result result = moveView(previousParent, newParentView, childView, index, layoutParams); |
| if (!result.isSuccess()) { |
| return result; |
| } |
| |
| result = render(false /*freshRender*/); |
| if (layoutParams != null && result.isSuccess()) { |
| result = result.getCopyWithData(layoutParams); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Moves a View from its current parent to a new given parent at a new given location, with |
| * an optional new {@link LayoutParams} instance |
| * |
| * @param previousParent the previous parent, still owning the child at the time of the call. |
| * @param newParent the new parent |
| * @param movedView the view to move |
| * @param index the new location in the new parent |
| * @param params an option (can be null) {@link LayoutParams} instance. |
| * |
| * @return a Result with {@link Status#SUCCESS} or |
| * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support |
| * adding views. |
| */ |
| private Result moveView(ViewGroup previousParent, final ViewGroup newParent, |
| final View movedView, final int index, final LayoutParams params) { |
| try { |
| // check if there is a transition on the previousParent. |
| LayoutTransition previousTransition = previousParent.getLayoutTransition(); |
| if (previousTransition != null) { |
| // in this case there is an animation. This means we have to wait for the child's |
| // parent reference to be null'ed out so that we can add it to the new parent. |
| // It is technically removed right before the DISAPPEARING animation is done (if |
| // the animation of this type is not null, otherwise it's after which is impossible |
| // to handle). |
| // Because there is no move animation, if the new parent is the same as the old |
| // parent, we need to wait until the CHANGE_DISAPPEARING animation is done before |
| // adding the child or the child will appear in its new location before the |
| // other children have made room for it. |
| |
| // add a listener to the transition to be notified of the actual removal. |
| previousTransition.addTransitionListener(new TransitionListener() { |
| private int mChangeDisappearingCount = 0; |
| |
| @Override |
| public void startTransition(LayoutTransition transition, ViewGroup container, |
| View view, int transitionType) { |
| if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { |
| mChangeDisappearingCount++; |
| } |
| } |
| |
| @Override |
| public void endTransition(LayoutTransition transition, ViewGroup container, |
| View view, int transitionType) { |
| if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { |
| mChangeDisappearingCount--; |
| } |
| |
| if (transitionType == LayoutTransition.CHANGE_DISAPPEARING && |
| mChangeDisappearingCount == 0) { |
| // add it to the parentView in the correct location |
| if (params != null) { |
| newParent.addView(movedView, index, params); |
| } else { |
| newParent.addView(movedView, index); |
| } |
| } |
| } |
| }); |
| |
| // remove the view from the current parent. |
| previousParent.removeView(movedView); |
| |
| // and return since adding the view to the new parent is done in the listener. |
| return SUCCESS.createResult(); |
| } else { |
| // standard code with no animation. pretty simple. |
| previousParent.removeView(movedView); |
| |
| // add it to the parentView in the correct location |
| if (params != null) { |
| newParent.addView(movedView, index, params); |
| } else { |
| newParent.addView(movedView, index); |
| } |
| |
| return SUCCESS.createResult(); |
| } |
| } catch (UnsupportedOperationException e) { |
| // looks like this is a view class that doesn't support children manipulation! |
| return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); |
| } |
| } |
| |
| /** |
| * Removes a child from its current parent. |
| * <p> |
| * {@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 RenderSession#removeChild(Object, IAnimationListener) |
| */ |
| public Result removeChild(final View childView, IAnimationListener listener) { |
| checkLock(); |
| |
| invalidateRenderingSize(); |
| |
| final ViewGroup parent = (ViewGroup) childView.getParent(); |
| |
| if (listener != null) { |
| new AnimationThread(this, "moveChild", listener) { |
| |
| @Override |
| public Result preAnimation() { |
| parent.setLayoutTransition(new LayoutTransition()); |
| return removeView(parent, childView); |
| } |
| |
| @Override |
| public void postAnimation() { |
| parent.setLayoutTransition(null); |
| } |
| }.start(); |
| |
| // always return success since the real status will come through the listener. |
| return SUCCESS.createResult(); |
| } |
| |
| Result result = removeView(parent, childView); |
| if (!result.isSuccess()) { |
| return result; |
| } |
| |
| return render(false /*freshRender*/); |
| } |
| |
| /** |
| * Removes a given view from its current parent. |
| * |
| * @param view the view to remove from its parent |
| * |
| * @return a Result with {@link Status#SUCCESS} or |
| * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support |
| * adding views. |
| */ |
| private Result removeView(ViewGroup parent, View view) { |
| try { |
| parent.removeView(view); |
| return SUCCESS.createResult(); |
| } catch (UnsupportedOperationException e) { |
| // looks like this is a view class that doesn't support children manipulation! |
| return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); |
| } |
| } |
| |
| |
| private void findBackground(RenderResources resources) { |
| if (!getParams().isBgColorOverridden()) { |
| mWindowBackground = resources.findItemInTheme("windowBackground", |
| true /*isFrameworkAttr*/); |
| if (mWindowBackground != null) { |
| mWindowBackground = resources.resolveResValue(mWindowBackground); |
| } |
| } |
| } |
| |
| private boolean hasSoftwareButtons() { |
| return getParams().getHardwareConfig().hasSoftwareButtons(); |
| } |
| |
| private void findStatusBar(RenderResources resources, DisplayMetrics metrics) { |
| boolean windowFullscreen = getBooleanThemeValue(resources, |
| "windowFullscreen", false, true); |
| |
| if (!windowFullscreen && !mWindowIsFloating) { |
| // default value |
| mStatusBarSize = DEFAULT_STATUS_BAR_HEIGHT; |
| |
| // get the real value |
| ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN, |
| "status_bar_height"); |
| |
| if (value != null) { |
| TypedValue typedValue = ResourceHelper.getValue("status_bar_height", |
| value.getValue(), true /*requireUnit*/); |
| if (typedValue != null) { |
| // compute the pixel value based on the display metrics |
| mStatusBarSize = (int)typedValue.getDimension(metrics); |
| } |
| } |
| } |
| } |
| |
| private void findActionBar(RenderResources resources, DisplayMetrics metrics) { |
| if (mWindowIsFloating) { |
| return; |
| } |
| |
| boolean windowActionBar = getBooleanThemeValue(resources, |
| "windowActionBar", true, !isThemeAppCompat(resources)); |
| |
| // if there's a value and it's false (default is true) |
| if (windowActionBar) { |
| |
| // default size of the window title bar |
| mActionBarSize = DEFAULT_TITLE_BAR_HEIGHT; |
| |
| // get value from the theme. |
| ResourceValue value = resources.findItemInTheme("actionBarSize", |
| true /*isFrameworkAttr*/); |
| |
| // resolve it |
| value = resources.resolveResValue(value); |
| |
| if (value != null) { |
| // get the numerical value, if available |
| TypedValue typedValue = ResourceHelper.getValue("actionBarSize", value.getValue(), |
| true /*requireUnit*/); |
| if (typedValue != null) { |
| // compute the pixel value based on the display metrics |
| mActionBarSize = (int)typedValue.getDimension(metrics); |
| } |
| } |
| } else { |
| // action bar overrides title bar so only look for this one if action bar is hidden |
| boolean windowNoTitle = getBooleanThemeValue(resources, "windowNoTitle", false, true); |
| |
| if (!windowNoTitle) { |
| |
| // default size of the window title bar |
| mTitleBarSize = DEFAULT_TITLE_BAR_HEIGHT; |
| |
| // get value from the theme. |
| ResourceValue value = resources.findItemInTheme("windowTitleSize", |
| true /*isFrameworkAttr*/); |
| |
| // resolve it |
| value = resources.resolveResValue(value); |
| |
| if (value != null) { |
| // get the numerical value, if available |
| TypedValue typedValue = ResourceHelper.getValue("windowTitleSize", |
| value.getValue(), true /*requireUnit*/); |
| if (typedValue != null) { |
| // compute the pixel value based on the display metrics |
| mTitleBarSize = (int)typedValue.getDimension(metrics); |
| } |
| } |
| } |
| |
| } |
| } |
| |
| private void findNavigationBar(RenderResources resources, DisplayMetrics metrics) { |
| if (hasSoftwareButtons() && !mWindowIsFloating) { |
| |
| // default value |
| mNavigationBarSize = 48; // ?? |
| |
| HardwareConfig hardwareConfig = getParams().getHardwareConfig(); |
| |
| boolean barOnBottom = true; |
| |
| if (hardwareConfig.getOrientation() == ScreenOrientation.LANDSCAPE) { |
| // compute the dp of the screen. |
| int shortSize = hardwareConfig.getScreenHeight(); |
| |
| // compute in dp |
| int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT / |
| hardwareConfig.getDensity().getDpiValue(); |
| |
| // 0-599dp: "phone" UI with bar on the side |
| // 600+dp: "tablet" UI with bar on the bottom |
| barOnBottom = shortSizeDp >= 600; |
| } |
| |
| if (barOnBottom) { |
| mNavigationBarOrientation = LinearLayout.HORIZONTAL; |
| } else { |
| mNavigationBarOrientation = LinearLayout.VERTICAL; |
| } |
| |
| // get the real value |
| ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN, |
| barOnBottom ? "navigation_bar_height" : "navigation_bar_width"); |
| |
| if (value != null) { |
| TypedValue typedValue = ResourceHelper.getValue("navigation_bar_height", |
| value.getValue(), true /*requireUnit*/); |
| if (typedValue != null) { |
| // compute the pixel value based on the display metrics |
| mNavigationBarSize = (int)typedValue.getDimension(metrics); |
| } |
| } |
| } |
| } |
| |
| private boolean isThemeAppCompat(RenderResources resources) { |
| // Ideally, we should check if the corresponding activity extends |
| // android.support.v7.app.ActionBarActivity, and not care about the theme name at all. |
| if (mIsThemeAppCompat == null) { |
| StyleResourceValue defaultTheme = resources.getDefaultTheme(); |
| // We can't simply check for parent using resources.themeIsParentOf() since the |
| // inheritance structure isn't really what one would expect. The first common parent |
| // between Theme.AppCompat.Light and Theme.AppCompat is Theme.Material (for v21). |
| boolean isThemeAppCompat = false; |
| for (int i = 0; i < 50; i++) { |
| if (defaultTheme == null) { |
| break; |
| } |
| // for loop ensures that we don't run into cyclic theme inheritance. |
| if (defaultTheme.getName().startsWith("Theme.AppCompat")) { |
| isThemeAppCompat = true; |
| break; |
| } |
| defaultTheme = resources.getParent(defaultTheme); |
| } |
| mIsThemeAppCompat = isThemeAppCompat; |
| } |
| return mIsThemeAppCompat; |
| } |
| |
| /** |
| * Looks for an attribute in the current theme. |
| * |
| * @param resources the render resources |
| * @param name the name of the attribute |
| * @param defaultValue the default value. |
| * @param isFrameworkAttr if the attribute is in android namespace |
| * @return the value of the attribute or the default one if not found. |
| */ |
| private boolean getBooleanThemeValue(RenderResources resources, |
| String name, boolean defaultValue, boolean isFrameworkAttr) { |
| |
| ResourceValue value = resources.findItemInTheme(name, isFrameworkAttr); |
| |
| // because it may reference something else, we resolve it. |
| value = resources.resolveResValue(value); |
| |
| // if there's no value, return the default. |
| if (value == null || value.getValue() == null) { |
| return defaultValue; |
| } |
| |
| return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue); |
| } |
| |
| /** |
| * Post process on a view hierarchy that was just inflated. |
| * <p/> |
| * 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. |
| */ |
| @SuppressWarnings("deprecation") // For the use of Pair |
| 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<View, Boolean> 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<View, Boolean> 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 (isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) { |
| RecyclerViewUtil.setAdapter(view, getContext(), getParams()); |
| } else if (view instanceof ViewGroup) { |
| 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; |
| } |
| RenderResources res = context.getRenderResources(); |
| String title = params.getAppLabel(); |
| ResourceValue titleValue = res.findResValue(title, false); |
| if (titleValue != null && titleValue.getValue() != null) { |
| title = titleValue.getValue(); |
| } |
| 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 vertical scroll position on all the components with the "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 void handleScrolling(View view) { |
| BridgeContext context = getContext(); |
| int scrollPos = context.getScrollYPos(view); |
| if (scrollPos != 0) { |
| if (view.isNestedScrollingEnabled()) { |
| int[] consumed = new int[2]; |
| if (view.startNestedScroll(DesignLibUtil.SCROLL_AXIS_VERTICAL)) { |
| view.dispatchNestedPreScroll(0, scrollPos, consumed, null); |
| view.dispatchNestedScroll(consumed[0], consumed[1], 0, scrollPos, null); |
| view.stopNestedScroll(); |
| scrollPos -= consumed[1]; |
| } |
| } |
| if (scrollPos != 0) { |
| view.scrollBy(0, scrollPos); |
| } else { |
| view.scrollBy(0, scrollPos); |
| } |
| } else { |
| view.scrollBy(0, scrollPos); |
| } |
| |
| if (!(view instanceof ViewGroup)) { |
| return; |
| } |
| ViewGroup group = (ViewGroup) view; |
| for (int i = 0; i < group.getChildCount(); i++) { |
| View child = group.getChildAt(i); |
| handleScrolling(child); |
| } |
| } |
| |
| /** |
| * Check if the object is an instance of a class named {@code className}. This doesn't work |
| * for interfaces. |
| */ |
| public static boolean isInstanceOf(Object object, String className) { |
| Class superClass = object.getClass(); |
| while (superClass != null) { |
| String name = superClass.getName(); |
| if (name.equals(className)) { |
| return true; |
| } |
| superClass = superClass.getSuperclass(); |
| } |
| return false; |
| } |
| |
| /** |
| * 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 |
| */ |
| 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. |
| tabHost.setup(); |
| |
| if (count == 0) { |
| // Create a dummy 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(new TabHost.TabContentFactory() { |
| @Override |
| public View createTabContent(String tag) { |
| return 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); |
| @SuppressWarnings("ConstantConditions") // child cannot be null. |
| int id = child.getId(); |
| @SuppressWarnings("deprecation") |
| Pair<ResourceType, String> resource = layoutlibCallback.resolveResourceId(id); |
| String name; |
| if (resource != null) { |
| name = resource.getSecond(); |
| } 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 offset an 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 offset, boolean setExtendedInfo, |
| boolean isContentFrame) { |
| ViewInfo result = createViewInfo(view, offset, setExtendedInfo, isContentFrame); |
| |
| if (view instanceof ViewGroup) { |
| ViewGroup group = ((ViewGroup) view); |
| result.setChildren(visitAllChildren(group, isContentFrame ? 0 : offset, |
| 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 offset an 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<ViewInfo> visitAllChildren(ViewGroup viewGroup, int offset, |
| boolean setExtendedInfo, boolean isContentFrame) { |
| if (viewGroup == null) { |
| return null; |
| } |
| |
| if (!isContentFrame) { |
| offset += viewGroup.getTop(); |
| } |
| |
| int childCount = viewGroup.getChildCount(); |
| if (viewGroup == mContentRoot) { |
| List<ViewInfo> childrenWithoutOffset = new ArrayList<ViewInfo>(childCount); |
| List<ViewInfo> childrenWithOffset = new ArrayList<ViewInfo>(childCount); |
| for (int i = 0; i < childCount; i++) { |
| ViewInfo[] childViewInfo = visitContentRoot(viewGroup.getChildAt(i), offset, |
| setExtendedInfo); |
| childrenWithoutOffset.add(childViewInfo[0]); |
| childrenWithOffset.add(childViewInfo[1]); |
| } |
| mViewInfoList = childrenWithOffset; |
| return childrenWithoutOffset; |
| } else { |
| List<ViewInfo> children = new ArrayList<ViewInfo>(childCount); |
| for (int i = 0; i < childCount; i++) { |
| children.add(visit(viewGroup.getChildAt(i), offset, 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 offset, boolean setExtendedInfo) { |
| ViewInfo[] result = new ViewInfo[2]; |
| if (view == null) { |
| return result; |
| } |
| |
| result[0] = createViewInfo(view, 0, setExtendedInfo, true); |
| result[1] = createViewInfo(view, offset, setExtendedInfo, true); |
| if (view instanceof ViewGroup) { |
| List<ViewInfo> children = visitAllChildren((ViewGroup) view, 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 offset an offset for the view bounds. Used only if view is part of the content frame. |
| */ |
| private ViewInfo createViewInfo(View view, int offset, boolean setExtendedInfo, |
| boolean isContentFrame) { |
| if (view == null) { |
| return null; |
| } |
| |
| ViewInfo result; |
| if (isContentFrame) { |
| // The view is part of the layout added by the user. Hence, |
| // the ViewCookie may be obtained only through the Context. |
| result = new ViewInfo(view.getClass().getName(), |
| getContext().getViewKey(view), |
| view.getLeft(), view.getTop() + offset, view.getRight(), |
| view.getBottom() + offset, 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. |
| ViewParent parent = view.getParent(); |
| 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; |
| } |
| |
| /** |
| * Creates the status bar with wifi and battery icons. |
| */ |
| private StatusBar createStatusBar(BridgeContext context, Density density, int direction, |
| boolean isRtlSupported, int platformVersion) throws XmlPullParserException { |
| StatusBar statusBar = new StatusBar(context, density, |
| direction, isRtlSupported, platformVersion); |
| statusBar.setLayoutParams( |
| new LinearLayout.LayoutParams( |
| LayoutParams.MATCH_PARENT, mStatusBarSize)); |
| return statusBar; |
| } |
| |
| /** |
| * Creates the navigation bar with back, home and recent buttons. |
| * |
| * @param isRtl true if the current locale is right-to-left |
| * @param isRtlSupported true is the project manifest declares that the application |
| * is RTL aware. |
| */ |
| private NavigationBar createNavigationBar(BridgeContext context, Density density, |
| boolean isRtl, boolean isRtlSupported, int simulatedPlatformVersion) |
| throws XmlPullParserException { |
| NavigationBar navigationBar = new NavigationBar(context, |
| density, mNavigationBarOrientation, isRtl, |
| isRtlSupported, simulatedPlatformVersion); |
| if (mNavigationBarOrientation == LinearLayout.VERTICAL) { |
| navigationBar.setLayoutParams(new LinearLayout.LayoutParams(mNavigationBarSize, |
| LayoutParams.MATCH_PARENT)); |
| } else { |
| navigationBar.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, |
| mNavigationBarSize)); |
| } |
| return navigationBar; |
| } |
| |
| private TitleBar createTitleBar(BridgeContext context, String title, |
| int simulatedPlatformVersion) |
| throws XmlPullParserException { |
| TitleBar titleBar = new TitleBar(context, title, simulatedPlatformVersion); |
| titleBar.setLayoutParams( |
| new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, mTitleBarSize)); |
| return titleBar; |
| } |
| |
| /** |
| * Creates the action bar. Also queries the project callback for missing information. |
| */ |
| private BridgeActionBar createActionBar(BridgeContext context, SessionParams params, |
| ViewGroup parentView) { |
| if (mIsThemeAppCompat == Boolean.TRUE) { |
| return new AppCompatActionBar(context, params, parentView); |
| } else { |
| return new FrameworkActionBar(context, params, parentView); |
| } |
| } |
| |
| public BufferedImage getImage() { |
| return mImage; |
| } |
| |
| public boolean isAlphaChannelImage() { |
| return mIsAlphaChannelImage; |
| } |
| |
| public List<ViewInfo> getViewInfos() { |
| return mViewInfoList; |
| } |
| |
| public List<ViewInfo> getSystemViewInfos() { |
| return mSystemViewInfoList; |
| } |
| |
| public Map<String, String> getDefaultProperties(Object viewObject) { |
| return getContext().getDefaultPropMap(viewObject); |
| } |
| |
| public void setScene(RenderSession session) { |
| mScene = session; |
| } |
| |
| public RenderSession getSession() { |
| return mScene; |
| } |
| } |