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