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