blob: fc4f9f15d6dcef91bfdeca6ced908ddb362eb48c [file] [log] [blame]
/*
* 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 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;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
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.IProjectCallback;
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.SessionParamsFlags;
import com.android.layoutlib.bridge.android.support.RecyclerViewUtil;
import com.android.layoutlib.bridge.bars.BridgeActionBar;
import com.android.layoutlib.bridge.bars.AppCompatActionBar;
import com.android.layoutlib.bridge.bars.Config;
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.bars.FrameworkActionBar;
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.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;
/**
* 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.getProjectCallback());
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.setProjectCallback(params.getProjectCallback());
String rootTag = params.getFlag(SessionParamsFlags.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.setProjectCallback(null);
// set the AttachInfo on the root view.
AttachInfo_Accessor.setAttachInfo(mViewRoot);
// post-inflate process. For now this supports TabHost/TabWidget
postInflateProcess(view, params.getProjectCallback(), isPreference ? view : null);
// 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);
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;
if (newRenderSize || mCanvas == null) {
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());
// create a Canvas around the Android bitmap
mCanvas = new Canvas(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.getProjectCallback().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, !isThemeAppCompat(resources));
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++) {
// 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);
if (defaultTheme == null) {
break;
}
}
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 projectCallback 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, IProjectCallback projectCallback, View skip)
throws PostInflateException {
if (view == skip) {
return;
}
if (view instanceof TabHost) {
setupTabHost((TabHost) view, projectCallback);
} 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 = params.getProjectCallback().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 /*attachToRoot*/, 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 /*attachToRoot*/, skipCallbackParser);
if (pair.getFirst() != null) {
list.addFooterView(pair.getFirst());
}
skipCallbackParser |= pair.getSecond();
}
}
if (view instanceof ExpandableListView) {
((ExpandableListView) view).setAdapter(
new FakeExpandableAdapter(
listRef, binding, params.getProjectCallback()));
} else {
((AbsListView) view).setAdapter(
new FakeAdapter(
listRef, binding, params.getProjectCallback()));
}
} else if (view instanceof AbsSpinner) {
((AbsSpinner) view).setAdapter(
new FakeAdapter(
listRef, binding, params.getProjectCallback()));
}
}
}
} 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, projectCallback, skip);
}
}
}
/**
* 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 projectCallback The project callback object to access the project R class.
* @throws PostInflateException
*/
private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback)
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))
.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 = projectCallback.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;
}
private 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;
}
}