Layout.java revision e5afc3117be394fdd92496b39e9bad248972902a
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.layoutlib.bridge.impl;
18
19import com.android.ide.common.rendering.api.HardwareConfig;
20import com.android.ide.common.rendering.api.RenderResources;
21import com.android.ide.common.rendering.api.ResourceValue;
22import com.android.ide.common.rendering.api.SessionParams;
23import com.android.ide.common.rendering.api.StyleResourceValue;
24import com.android.internal.util.XmlUtils;
25import com.android.layoutlib.bridge.Bridge;
26import com.android.layoutlib.bridge.android.BridgeContext;
27import com.android.layoutlib.bridge.bars.AppCompatActionBar;
28import com.android.layoutlib.bridge.bars.BridgeActionBar;
29import com.android.layoutlib.bridge.bars.Config;
30import com.android.layoutlib.bridge.bars.FrameworkActionBar;
31import com.android.layoutlib.bridge.bars.NavigationBar;
32import com.android.layoutlib.bridge.bars.StatusBar;
33import com.android.layoutlib.bridge.bars.TitleBar;
34import com.android.resources.Density;
35import com.android.resources.ResourceType;
36import com.android.resources.ScreenOrientation;
37
38import android.annotation.NonNull;
39import android.graphics.drawable.Drawable;
40import android.util.DisplayMetrics;
41import android.util.TypedValue;
42import android.view.View;
43import android.widget.FrameLayout;
44import android.widget.LinearLayout;
45import android.widget.RelativeLayout;
46
47import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
48import static android.widget.LinearLayout.VERTICAL;
49
50/**
51 * The Layout used to create the system decor.
52 *
53 * The layout inflated will contain a content frame where the user's layout can be inflated.
54 * <pre>
55 *  +-------------------------------------------------+---+
56 *  | Status bar                                      | N |
57 *  +-------------------------------------------------+ a |
58 *  | Title/Action bar (optional)                     | v |
59 *  +-------------------------------------------------+   |
60 *  | Content, vertical extending                     | b |
61 *  |                                                 | a |
62 *  |                                                 | r |
63 *  +-------------------------------------------------+---+
64 * </pre>
65 * or
66 * <pre>
67 *  +-------------------------------------+
68 *  | Status bar                          |
69 *  +-------------------------------------+
70 *  | Title/Action bar (optional)         |
71 *  +-------------------------------------+
72 *  | Content, vertical extending         |
73 *  |                                     |
74 *  |                                     |
75 *  +-------------------------------------+
76 *  | Nav bar                             |
77 *  +-------------------------------------+
78 * </pre>
79 *
80 */
81class Layout extends RelativeLayout {
82
83    // Theme attributes used for configuring appearance of the system decor.
84    private static final String ATTR_WINDOW_FLOATING = "windowIsFloating";
85    private static final String ATTR_WINDOW_BACKGROUND = "windowBackground";
86    private static final String ATTR_WINDOW_FULL_SCREEN = "windowFullScreen";
87    private static final String ATTR_NAV_BAR_HEIGHT = "navigation_bar_height";
88    private static final String ATTR_NAV_BAR_WIDTH = "navigation_bar_width";
89    private static final String ATTR_STATUS_BAR_HEIGHT = "status_bar_height";
90    private static final String ATTR_WINDOW_ACTION_BAR = "windowActionBar";
91    private static final String ATTR_ACTION_BAR_SIZE = "actionBarSize";
92    private static final String ATTR_WINDOW_NO_TITLE = "windowNoTitle";
93    private static final String ATTR_WINDOW_TITLE_SIZE = "windowTitleSize";
94    private static final String PREFIX_THEME_APPCOMPAT = "Theme.AppCompat";
95
96    // Default sizes
97    private static final int DEFAULT_STATUS_BAR_HEIGHT = 25;
98    private static final int DEFAULT_TITLE_BAR_HEIGHT = 25;
99    private static final int DEFAULT_NAV_BAR_SIZE = 48;
100
101    // Ids assigned to components created. This is so that we can refer to other components in
102    // layout params.
103    private static final String ID_NAV_BAR = "navBar";
104    private static final String ID_STATUS_BAR = "statusBar";
105    private static final String ID_TITLE_BAR = "titleBar";
106    // Prefix used with the above ids in order to make them unique in framework namespace.
107    private static final String ID_PREFIX = "android_layoutlib_";
108
109    /**
110     * Temporarily store the builder so that it doesn't have to be passed to all methods used
111     * during inflation.
112     */
113    private Builder mBuilder;
114
115    /**
116     * This holds user's layout.
117     */
118    private FrameLayout mContentRoot;
119
120    public Layout(@NonNull Builder builder) {
121        super(builder.mContext);
122        mBuilder = builder;
123        if (builder.mWindowBackground != null) {
124            Drawable d = ResourceHelper.getDrawable(builder.mWindowBackground, builder.mContext);
125            setBackground(d);
126        }
127
128        int simulatedPlatformVersion = getParams().getSimulatedPlatformVersion();
129        HardwareConfig hwConfig = getParams().getHardwareConfig();
130        Density density = hwConfig.getDensity();
131        boolean isRtl = Bridge.isLocaleRtl(getParams().getLocale());
132
133        NavigationBar navBar = null;
134        if (Config.showOnScreenNavBar(simulatedPlatformVersion) && mBuilder.hasSoftwareButtons() &&
135                builder.isNavBarVertical()) {
136            navBar = createNavBar(getContext(), density, isRtl, getParams().isRtlSupported(),
137                    simulatedPlatformVersion);
138        }
139
140        StatusBar statusBar = null;
141        if (builder.mStatusBarSize > 0) {
142            statusBar = createStatusBar(getContext(), density, isRtl, getParams().isRtlSupported(),
143                    simulatedPlatformVersion);
144        }
145
146        View actionBar = null;
147        TitleBar titleBar = null;
148        if (builder.mActionBarSize > 0) {
149            BridgeActionBar bar = createActionBar(getContext(), getParams());
150            mContentRoot = bar.getContentRoot();
151            actionBar = bar.getRootView();
152        } else if (mBuilder.mTitleBarSize > 0) {
153            titleBar = createTitleBar(getContext(), getParams().getAppLabel(),
154                    simulatedPlatformVersion);
155        }
156
157        addViews(titleBar, mContentRoot == null ? (mContentRoot = createContentFrame()) : actionBar,
158                statusBar, navBar);
159        // Done with the builder. Don't hold a reference to it.
160        mBuilder = null;
161     }
162
163    @NonNull
164    private FrameLayout createContentFrame() {
165        FrameLayout contentRoot = new FrameLayout(getContext());
166        LayoutParams params =
167                new LayoutParams(MATCH_PARENT, MATCH_PARENT);
168        int rule = mBuilder.isNavBarVertical() ? START_OF : ABOVE;
169        if (mBuilder.mNavBarSize > 0) {
170            params.addRule(rule, getId(ID_NAV_BAR));
171        }
172        int below = -1;
173        if (mBuilder.mActionBarSize <= 0 && mBuilder.mTitleBarSize > 0) {
174            below = getId(ID_TITLE_BAR);
175        } else if (mBuilder.mStatusBarSize > 0) {
176            below = getId(ID_STATUS_BAR);
177        }
178        if (below != -1) {
179            params.addRule(BELOW, below);
180        }
181
182        return contentRoot;
183    }
184
185    @NonNull
186    public FrameLayout getContentRoot() {
187        return mContentRoot;
188    }
189
190    @NonNull
191    private SessionParams getParams() {
192        return mBuilder.mParams;
193    }
194
195    @NonNull
196    @Override
197    public BridgeContext getContext(){
198        return (BridgeContext) super.getContext();
199    }
200
201    /**
202     * @param isRtl    whether the current locale is an RTL locale.
203     * @param isRtlSupported    whether the applications supports RTL (i.e. has supportsRtl=true
204     * in the manifest and targetSdkVersion >= 17.
205     */
206    @NonNull
207    private StatusBar createStatusBar(BridgeContext context, Density density, boolean isRtl,
208            boolean isRtlSupported, int simulatedPlatformVersion) {
209        StatusBar statusBar =
210                new StatusBar(context, density, isRtl, isRtlSupported, simulatedPlatformVersion);
211        LayoutParams params = new LayoutParams(MATCH_PARENT, mBuilder.mStatusBarSize);
212        if (mBuilder.isNavBarVertical()) {
213            params.addRule(START_OF, getId(ID_NAV_BAR));
214        }
215        statusBar.setLayoutParams(params);
216        statusBar.setId(getId(ID_STATUS_BAR));
217        return statusBar;
218    }
219
220    private BridgeActionBar createActionBar(@NonNull BridgeContext context,
221            @NonNull SessionParams params) {
222        BridgeActionBar actionBar;
223        if (mBuilder.isThemeAppCompat()) {
224            actionBar = new AppCompatActionBar(context, params);
225        } else {
226            actionBar = new FrameworkActionBar(context, params);
227        }
228        LayoutParams layoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
229        int rule = mBuilder.isNavBarVertical() ? START_OF : ABOVE;
230        if (mBuilder.mNavBarSize > 0) {
231            layoutParams.addRule(rule, getId(ID_NAV_BAR));
232        }
233        if (mBuilder.mStatusBarSize > 0) {
234            layoutParams.addRule(BELOW, getId(ID_STATUS_BAR));
235        }
236        actionBar.getRootView().setLayoutParams(layoutParams);
237        actionBar.createMenuPopup();
238        return actionBar;
239    }
240
241
242    @NonNull
243    private TitleBar createTitleBar(BridgeContext context, String title,
244            int simulatedPlatformVersion) {
245        TitleBar titleBar = new TitleBar(context, title, simulatedPlatformVersion);
246        LayoutParams params = new LayoutParams(MATCH_PARENT, mBuilder.mTitleBarSize);
247        if (mBuilder.mStatusBarSize > 0) {
248            params.addRule(BELOW, getId(ID_STATUS_BAR));
249        }
250        if (mBuilder.isNavBarVertical()) {
251            params.addRule(START_OF, getId(ID_NAV_BAR));
252        }
253        titleBar.setLayoutParams(params);
254        titleBar.setId(getId(ID_TITLE_BAR));
255        return titleBar;
256    }
257
258    /**
259     * @param isRtl    whether the current locale is an RTL locale.
260     * @param isRtlSupported    whether the applications supports RTL (i.e. has supportsRtl=true
261     * in the manifest and targetSdkVersion >= 17.
262     */
263    @NonNull
264    private NavigationBar createNavBar(BridgeContext context, Density density, boolean isRtl,
265            boolean isRtlSupported, int simulatedPlatformVersion) {
266        int orientation = mBuilder.mNavBarOrientation;
267        int size = mBuilder.mNavBarSize;
268        NavigationBar navBar = new NavigationBar(context, density, orientation, isRtl,
269                isRtlSupported, simulatedPlatformVersion);
270        boolean isVertical = mBuilder.isNavBarVertical();
271        int w = isVertical ? size : MATCH_PARENT;
272        int h = isVertical ? MATCH_PARENT : size;
273        LayoutParams params = new LayoutParams(w, h);
274        params.addRule(isVertical ? ALIGN_PARENT_END : ALIGN_PARENT_BOTTOM);
275        navBar.setLayoutParams(params);
276        navBar.setId(getId(ID_NAV_BAR));
277        return navBar;
278    }
279
280    private void addViews(@NonNull View... views) {
281        for (View view : views) {
282            if (view != null) {
283                addView(view);
284            }
285        }
286    }
287
288    private int getId(String name) {
289        return Bridge.getResourceId(ResourceType.ID, ID_PREFIX + name);
290    }
291
292    /**
293     * A helper class to help initialize the Layout.
294     */
295    static class Builder {
296        @NonNull
297        private final SessionParams mParams;
298        @NonNull
299        private final BridgeContext mContext;
300        private final RenderResources mResources;
301
302        private final boolean mWindowIsFloating;
303        private ResourceValue mWindowBackground;
304        private int mStatusBarSize;
305        private int mNavBarSize;
306        private int mNavBarOrientation;
307        private int mActionBarSize;
308        private int mTitleBarSize;
309
310        private Boolean mIsThemeAppCompat;
311
312        public Builder(@NonNull SessionParams params, @NonNull BridgeContext context) {
313            mParams = params;
314            mContext = context;
315            mResources = mParams.getResources();
316            mWindowIsFloating = getBooleanThemeValue(mResources, ATTR_WINDOW_FLOATING, true, true);
317
318            findBackground();
319            findStatusBar();
320            findActionBar();
321            findNavBar();
322        }
323
324        public boolean isNavBarVertical() {
325            return mNavBarOrientation == VERTICAL;
326        }
327
328        private void findBackground() {
329            if (!mParams.isBgColorOverridden()) {
330                mWindowBackground = mResources.findItemInTheme(ATTR_WINDOW_BACKGROUND, true);
331                mWindowBackground = mResources.resolveResValue(mWindowBackground);
332            }
333        }
334
335        private void findStatusBar() {
336            boolean windowFullScreen =
337                    getBooleanThemeValue(mResources, ATTR_WINDOW_FULL_SCREEN, true, false);
338            if (!windowFullScreen && !mWindowIsFloating) {
339                mStatusBarSize =
340                        getDimension(ATTR_STATUS_BAR_HEIGHT, true, DEFAULT_STATUS_BAR_HEIGHT);
341            }
342        }
343
344        private void  findActionBar() {
345            if (mWindowIsFloating) {
346                return;
347            }
348            // Check if an actionbar is needed
349            boolean windowActionBar = getBooleanThemeValue(mResources, ATTR_WINDOW_ACTION_BAR,
350                    isThemeAppCompat(), true);
351            if (windowActionBar) {
352                mActionBarSize = getDimension(ATTR_ACTION_BAR_SIZE, true, DEFAULT_TITLE_BAR_HEIGHT);
353            } else {
354                // Maybe the gingerbread era title bar is needed
355                boolean windowNoTitle =
356                        getBooleanThemeValue(mResources, ATTR_WINDOW_NO_TITLE, true, false);
357                if (!windowNoTitle) {
358                    mTitleBarSize =
359                            getDimension(ATTR_WINDOW_TITLE_SIZE, true, DEFAULT_TITLE_BAR_HEIGHT);
360                }
361            }
362        }
363
364        private void findNavBar() {
365            if (hasSoftwareButtons() && !mWindowIsFloating) {
366
367                // get orientation
368                HardwareConfig hwConfig = mParams.getHardwareConfig();
369                boolean barOnBottom = true;
370
371                if (hwConfig.getOrientation() == ScreenOrientation.LANDSCAPE) {
372                    int shortSize = hwConfig.getScreenHeight();
373                    int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT /
374                            hwConfig.getDensity().getDpiValue();
375
376                    // 0-599dp: "phone" UI with bar on the side
377                    // 600+dp: "tablet" UI with bar on the bottom
378                    barOnBottom = shortSizeDp >= 600;
379                }
380
381                mNavBarOrientation = barOnBottom ? LinearLayout.HORIZONTAL : VERTICAL;
382                mNavBarSize = getDimension(barOnBottom ? ATTR_NAV_BAR_HEIGHT : ATTR_NAV_BAR_WIDTH,
383                        true, DEFAULT_NAV_BAR_SIZE);
384            }
385        }
386
387        private int getDimension(String attr, boolean isFramework, int defaultValue) {
388            ResourceValue value = mResources.findItemInTheme(attr, isFramework);
389            value = mResources.resolveResValue(value);
390            if (value != null) {
391                TypedValue typedValue = ResourceHelper.getValue(attr, value.getValue(), true);
392                if (typedValue != null) {
393                    return (int) typedValue.getDimension(mContext.getMetrics());
394                }
395            }
396            return defaultValue;
397        }
398
399        /**
400         * Looks for an attribute in the current theme.
401         *
402         * @param resources the render resources
403         * @param name the name of the attribute
404         * @param defaultValue the default value.
405         * @param isFrameworkAttr if the attribute is in android namespace
406         * @return the value of the attribute or the default one if not found.
407         */
408        static boolean getBooleanThemeValue(@NonNull RenderResources resources, String name,
409                boolean isFrameworkAttr, boolean defaultValue) {
410            ResourceValue value = resources.findItemInTheme(name, isFrameworkAttr);
411            value = resources.resolveResValue(value);
412            if (value == null) {
413                return defaultValue;
414            }
415            return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue);
416        }
417
418        private boolean hasSoftwareButtons() {
419            return mParams.getHardwareConfig().hasSoftwareButtons();
420        }
421
422        private boolean isThemeAppCompat() {
423            // If a cached value exists, return it.
424            if (mIsThemeAppCompat != null) {
425                return mIsThemeAppCompat;
426            }
427            // Ideally, we should check if the corresponding activity extends
428            // android.support.v7.app.ActionBarActivity, and not care about the theme name at all.
429            StyleResourceValue defaultTheme = mResources.getDefaultTheme();
430            // We can't simply check for parent using resources.themeIsParentOf() since the
431            // inheritance structure isn't really what one would expect. The first common parent
432            // between Theme.AppCompat.Light and Theme.AppCompat is Theme.Material (for v21).
433            boolean isThemeAppCompat = false;
434            for (int i = 0; i < 50; i++) {
435                if (defaultTheme == null) {
436                    break;
437                }
438                // for loop ensures that we don't run into cyclic theme inheritance.
439                if (defaultTheme.getName().startsWith(PREFIX_THEME_APPCOMPAT)) {
440                    isThemeAppCompat = true;
441                    break;
442                }
443                defaultTheme = mResources.getParent(defaultTheme);
444            }
445            mIsThemeAppCompat = isThemeAppCompat;
446            return isThemeAppCompat;
447        }
448    }
449}
450