/* * 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.LayoutLog; 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.SessionParams; import com.android.ide.common.rendering.api.SessionParams.RenderingMode; import com.android.ide.common.rendering.api.ViewInfo; import com.android.ide.common.rendering.api.ViewType; import com.android.internal.view.menu.ActionMenuItemView; import com.android.internal.view.menu.BridgeMenuItemImpl; import com.android.internal.view.menu.IconMenuItemView; import com.android.internal.view.menu.ListMenuItemView; import com.android.internal.view.menu.MenuItemImpl; import com.android.internal.view.menu.MenuView; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; import com.android.layoutlib.bridge.android.RenderParamsFlags; import com.android.layoutlib.bridge.android.graphics.NopCanvas; import com.android.layoutlib.bridge.android.support.DesignLibUtil; import com.android.layoutlib.bridge.android.support.FragmentTabHostUtil; import com.android.layoutlib.bridge.android.support.SupportPreferencesUtil; import com.android.layoutlib.bridge.impl.binding.FakeAdapter; import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter; import com.android.layoutlib.bridge.util.ReflectionUtils; import com.android.resources.ResourceType; import com.android.tools.layoutlib.java.System_Delegate; import com.android.util.Pair; import com.android.util.PropertiesMap; 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.os.Looper; import android.preference.Preference_Delegate; import android.view.AttachInfo_Accessor; import android.view.BridgeInflater; import android.view.Choreographer_Delegate; import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.MarginLayoutParams; import android.view.ViewParent; import android.widget.AbsListView; import android.widget.AbsSpinner; import android.widget.ActionMenuView; import android.widget.AdapterView; import android.widget.ExpandableListView; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.QuickContactBadge; import android.widget.TabHost; import android.widget.TabHost.TabSpec; import android.widget.TabWidget; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; import java.util.Map; import static com.android.ide.common.rendering.api.Result.Status.ERROR_INFLATION; import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED; import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; import static com.android.layoutlib.bridge.util.ReflectionUtils.isInstanceOf; /** * Class implementing the render session. *
* A session is a stateful representation of a layout file. It is initialized with data coming * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then * be done on the layout. */ public class RenderSessionImpl extends RenderAction
* This must be followed by a call to {@link RenderSessionImpl#init(long)},
* which act as a
* call to {@link RenderSessionImpl#acquire(long)}
*
* @see Bridge#createSession(SessionParams)
*/
public RenderSessionImpl(SessionParams params) {
super(new SessionParams(params));
}
/**
* Initializes and acquires the scene, creating various Android objects such as context,
* inflater, and parser.
*
* @param timeout the time to wait if another rendering is happening.
*
* @return whether the scene was prepared
*
* @see #acquire(long)
* @see #release()
*/
@Override
public Result init(long timeout) {
Result result = super.init(timeout);
if (!result.isSuccess()) {
return result;
}
SessionParams params = getParams();
BridgeContext context = getContext();
// use default of true in case it's not found to use alpha by default
mIsAlphaChannelImage = ResourceHelper.getBooleanThemeValue(params.getResources(),
"windowIsFloating", true, true);
mLayoutBuilder = new Layout.Builder(params, context);
// build the inflater and parser.
mInflater = new BridgeInflater(context, params.getLayoutlibCallback());
context.setBridgeInflater(mInflater);
mBlockParser = new BridgeXmlBlockParser(params.getLayoutDescription(), context, false);
return SUCCESS.createResult();
}
/**
* Measures the the current layout if needed (see {@link #invalidateRenderingSize}).
*/
private void measureLayout(@NonNull SessionParams params) {
// only do the screen measure when needed.
if (mMeasuredScreenWidth != -1) {
return;
}
RenderingMode renderingMode = params.getRenderingMode();
HardwareConfig hardwareConfig = params.getHardwareConfig();
mNewRenderSize = true;
mMeasuredScreenWidth = hardwareConfig.getScreenWidth();
mMeasuredScreenHeight = hardwareConfig.getScreenHeight();
if (renderingMode != RenderingMode.NORMAL) {
int widthMeasureSpecMode = renderingMode.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.
View measuredView = mContentRoot.getChildAt(0);
// first measure the full layout, with EXACTLY to get the size of the
// content as it is inside the decor/dialog
@SuppressWarnings("deprecation")
Pair
* {@link #acquire(long)} must have been called before this.
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene, or if {@link #init(long)} was not called.
*/
public Result inflate() {
checkLock();
try {
mViewRoot = new Layout(mLayoutBuilder);
mLayoutBuilder = null; // Done with the builder.
mContentRoot = ((Layout) mViewRoot).getContentRoot();
SessionParams params = getParams();
BridgeContext context = getContext();
if (Bridge.isLocaleRtl(params.getLocale())) {
if (!params.isRtlSupported()) {
Bridge.getLog().warning(LayoutLog.TAG_RTL_NOT_ENABLED,
"You are using a right-to-left " +
"(RTL) locale but RTL is not enabled", null);
} else if (params.getSimulatedPlatformVersion() < 17) {
// This will render ok because we are using the latest layoutlib but at least
// warn the user that this might fail in a real device.
Bridge.getLog().warning(LayoutLog.TAG_RTL_NOT_SUPPORTED, "You are using a " +
"right-to-left " +
"(RTL) locale but RTL is not supported for API level < 17", null);
}
}
// 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) {
// First try to use the support library inflater. If something fails, fallback
// to the system preference inflater.
view = SupportPreferencesUtil.inflatePreference(getContext(), mBlockParser,
mContentRoot);
if (view == null) {
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);
mInflater.onDoneInflation();
setActiveToolbar(view, context, params);
measureLayout(params);
measureView(mViewRoot, null /*measuredView*/,
mMeasuredScreenWidth, MeasureSpec.EXACTLY,
mMeasuredScreenHeight, MeasureSpec.EXACTLY);
mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
mSystemViewInfoList =
visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(),
false);
Choreographer_Delegate.clearFrames();
return SUCCESS.createResult();
} catch (PostInflateException e) {
return ERROR_INFLATION.createResult(e.getMessage(), e);
} catch (Throwable e) {
// get the real cause of the exception.
Throwable t = e;
while (t.getCause() != null) {
t = t.getCause();
}
return ERROR_INFLATION.createResult(t.getMessage(), t);
}
}
/**
* Sets the time for which the next frame will be selected. The time is the elapsed time from
* the current system nanos time. You
*/
public void setElapsedFrameTimeNanos(long nanos) {
mElapsedFrameTimeNanos = nanos;
}
/**
* Runs a layout pass for the given view root
*/
private static void doLayout(@NonNull BridgeContext context, @NonNull ViewGroup viewRoot,
int width, int height) {
// measure again with the size we need
// This must always be done before the call to layout
measureView(viewRoot, null /*measuredView*/,
width, MeasureSpec.EXACTLY,
height, MeasureSpec.EXACTLY);
// now do the layout.
viewRoot.layout(0, 0, width, height);
handleScrolling(context, viewRoot);
}
/**
* Renders the given view hierarchy to the passed canvas and returns the result of the render
* operation.
* @param canvas an optional canvas to render the views to. If null, only the measure and
* layout steps will be executed.
*/
private static Result renderAndBuildResult(@NonNull ViewGroup viewRoot, @Nullable Canvas canvas) {
if (canvas == null) {
return SUCCESS.createResult();
}
AttachInfo_Accessor.dispatchOnPreDraw(viewRoot);
viewRoot.draw(canvas);
return SUCCESS.createResult();
}
/**
* Renders the scene.
*
* {@link #acquire(long)} must have been called before this.
*
* @param freshRender whether the render is a new one and should erase the existing bitmap (in
* the case where bitmaps are reused). This is typically needed when not playing
* animations.)
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene, or if {@link #acquire(long)} was not called.
*
* @see SessionParams#getRenderingMode()
* @see RenderSession#render(long)
*/
public Result render(boolean freshRender) {
return renderAndBuildResult(freshRender, false);
}
/**
* Measures the layout
*
* {@link #acquire(long)} must have been called before this.
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene, or if {@link #acquire(long)} was not called.
*
* @see SessionParams#getRenderingMode()
* @see RenderSession#render(long)
*/
public Result measure() {
return renderAndBuildResult(false, true);
}
/**
* Renders the scene.
*
* {@link #acquire(long)} must have been called before this.
*
* @param freshRender whether the render is a new one and should erase the existing bitmap (in
* the case where bitmaps are reused). This is typically needed when not playing
* animations.)
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene, or if {@link #acquire(long)} was not called.
*
* @see SessionParams#getRenderingMode()
* @see RenderSession#render(long)
*/
private Result renderAndBuildResult(boolean freshRender, boolean onlyMeasure) {
checkLock();
SessionParams params = getParams();
try {
if (mViewRoot == null) {
return ERROR_NOT_INFLATED.createResult();
}
measureLayout(params);
HardwareConfig hardwareConfig = params.getHardwareConfig();
Result renderResult = SUCCESS.createResult();
if (onlyMeasure) {
// delete the canvas and image to reset them on the next full rendering
mImage = null;
mCanvas = null;
doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight);
} else {
// draw the views
// create the BufferedImage into which the layout will be rendered.
boolean newImage = false;
// When disableBitmapCaching is true, we do not reuse mImage and
// we create a new one in every render.
// This is useful when mImage is just a wrapper of Graphics2D so
// it doesn't get cached.
boolean disableBitmapCaching = Boolean.TRUE.equals(params.getFlag(
RenderParamsFlags.FLAG_KEY_DISABLE_BITMAP_CACHING));
if (mNewRenderSize || mCanvas == null || disableBitmapCaching) {
mNewRenderSize = false;
if (params.getImageFactory() != null) {
mImage = params.getImageFactory().getImage(
mMeasuredScreenWidth,
mMeasuredScreenHeight);
} else {
mImage = new BufferedImage(
mMeasuredScreenWidth,
mMeasuredScreenHeight,
BufferedImage.TYPE_INT_ARGB);
newImage = true;
}
if (params.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();
}
doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight);
if (mElapsedFrameTimeNanos >= 0) {
long initialTime = System_Delegate.nanoTime();
if (!mFirstFrameExecuted) {
// We need to run an initial draw call to initialize the animations
renderAndBuildResult(mViewRoot, NOP_CANVAS);
// The first frame will initialize the animations
Choreographer_Delegate.doFrame(initialTime);
mFirstFrameExecuted = true;
}
// Second frame will move the animations
Choreographer_Delegate.doFrame(initialTime + mElapsedFrameTimeNanos);
}
renderResult = renderAndBuildResult(mViewRoot, mCanvas);
}
mSystemViewInfoList =
visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(),
false);
// success!
return renderResult;
} catch (Throwable e) {
// get the real cause of the exception.
Throwable t = e;
while (t.getCause() != null) {
t = t.getCause();
}
return ERROR_UNKNOWN.createResult(t.getMessage(), t);
}
}
/**
* Executes {@link View#measure(int, int)} on a given view with the given parameters (used
* to create measure specs with {@link MeasureSpec#makeMeasureSpec(int, int)}.
*
* if measuredView is non null, the method returns a {@link Pair} of (width, height)
* for the view (using {@link View#getMeasuredWidth()} and {@link View#getMeasuredHeight()}).
*
* @param viewToMeasure the view on which to execute measure().
* @param measuredView if non null, the view to query for its measured width/height.
* @param width the width to use in the MeasureSpec.
* @param widthMode the MeasureSpec mode to use for the width.
* @param height the height to use in the MeasureSpec.
* @param heightMode the MeasureSpec mode to use for the height.
* @return the measured width/height if measuredView is non-null, null otherwise.
*/
@SuppressWarnings("deprecation") // For the use of Pair
private static Pair