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 */
16package android.support.v7.widget;
17
18import android.content.Context;
19import android.content.res.TypedArray;
20import android.os.Build;
21import android.support.annotation.RestrictTo;
22import android.support.v4.content.res.ConfigurationHelper;
23import android.support.v4.view.ViewCompat;
24import android.support.v7.appcompat.R;
25import android.util.AttributeSet;
26import android.view.Gravity;
27import android.view.View;
28import android.widget.LinearLayout;
29
30import static android.support.annotation.RestrictTo.Scope.GROUP_ID;
31
32/**
33 * An extension of LinearLayout that automatically switches to vertical
34 * orientation when it can't fit its child views horizontally.
35 *
36 * @hide
37 */
38@RestrictTo(GROUP_ID)
39public class ButtonBarLayout extends LinearLayout {
40    // Whether to allow vertically stacked button bars. This is disabled for
41    // configurations with a small (e.g. less than 320dp) screen height. -->
42    private static final int ALLOW_STACKING_MIN_HEIGHT_DP = 320;
43
44    /** Whether the current configuration allows stacking. */
45    private boolean mAllowStacking;
46    private int mLastWidthSize = -1;
47
48    public ButtonBarLayout(Context context, AttributeSet attrs) {
49        super(context, attrs);
50        final boolean allowStackingDefault =
51                ConfigurationHelper.getScreenHeightDp(getResources())
52                        >= ALLOW_STACKING_MIN_HEIGHT_DP;
53        final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ButtonBarLayout);
54        mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking,
55                allowStackingDefault);
56        ta.recycle();
57    }
58
59    public void setAllowStacking(boolean allowStacking) {
60        if (mAllowStacking != allowStacking) {
61            mAllowStacking = allowStacking;
62            if (!mAllowStacking && getOrientation() == LinearLayout.VERTICAL) {
63                setStacked(false);
64            }
65            requestLayout();
66        }
67    }
68
69    @Override
70    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
71        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
72        if (mAllowStacking) {
73            if (widthSize > mLastWidthSize && isStacked()) {
74                // We're being measured wider this time, try un-stacking.
75                setStacked(false);
76            }
77            mLastWidthSize = widthSize;
78        }
79        boolean needsRemeasure = false;
80        // If we're not stacked, make sure the measure spec is AT_MOST rather
81        // than EXACTLY. This ensures that we'll still get TOO_SMALL so that we
82        // know to stack the buttons.
83        final int initialWidthMeasureSpec;
84        if (!isStacked() && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
85            initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
86            // We'll need to remeasure again to fill excess space.
87            needsRemeasure = true;
88        } else {
89            initialWidthMeasureSpec = widthMeasureSpec;
90        }
91        super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec);
92        if (mAllowStacking && !isStacked()) {
93            final boolean stack;
94
95            if (Build.VERSION.SDK_INT >= 11) {
96                // On API v11+ we can use MEASURED_STATE_MASK and MEASURED_STATE_TOO_SMALL
97                final int measuredWidth = ViewCompat.getMeasuredWidthAndState(this);
98                final int measuredWidthState = measuredWidth & ViewCompat.MEASURED_STATE_MASK;
99                stack = measuredWidthState == ViewCompat.MEASURED_STATE_TOO_SMALL;
100            } else {
101                // Before that we need to manually total up the children's preferred width.
102                // This isn't perfect but works well enough for a workaround.
103                int childWidthTotal = 0;
104                for (int i = 0, count = getChildCount(); i < count; i++) {
105                    childWidthTotal += getChildAt(i).getMeasuredWidth();
106                }
107                stack = (childWidthTotal + getPaddingLeft() + getPaddingRight()) > widthSize;
108            }
109
110            if (stack) {
111                setStacked(true);
112                // Measure again in the new orientation.
113                needsRemeasure = true;
114            }
115        }
116        if (needsRemeasure) {
117            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
118        }
119    }
120
121    private void setStacked(boolean stacked) {
122        setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
123        setGravity(stacked ? Gravity.RIGHT : Gravity.BOTTOM);
124        final View spacer = findViewById(R.id.spacer);
125        if (spacer != null) {
126            spacer.setVisibility(stacked ? View.GONE : View.INVISIBLE);
127        }
128        // Reverse the child order. This is specific to the Material button
129        // bar's layout XML and will probably not generalize.
130        final int childCount = getChildCount();
131        for (int i = childCount - 2; i >= 0; i--) {
132            bringChildToFront(getChildAt(i));
133        }
134    }
135
136    private boolean isStacked() {
137        return getOrientation() == LinearLayout.VERTICAL;
138    }
139}
140