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 androidx.appcompat.widget;
17 
18 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
19 
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.util.AttributeSet;
23 import android.view.Gravity;
24 import android.view.View;
25 import android.widget.LinearLayout;
26 
27 import androidx.annotation.RestrictTo;
28 import androidx.appcompat.R;
29 import androidx.core.view.ViewCompat;
30 
31 /**
32  * An extension of LinearLayout that automatically switches to vertical
33  * orientation when it can't fit its child views horizontally.
34  *
35  * @hide
36  */
37 @RestrictTo(LIBRARY_GROUP)
38 public class ButtonBarLayout extends LinearLayout {
39     /** Amount of the second button to "peek" above the fold when stacked. */
40     private static final int PEEK_BUTTON_DP = 16;
41 
42     /** Whether the current configuration allows stacking. */
43     private boolean mAllowStacking;
44 
45     private int mLastWidthSize = -1;
46 
47     private int mMinimumHeight = 0;
48 
ButtonBarLayout(Context context, AttributeSet attrs)49     public ButtonBarLayout(Context context, AttributeSet attrs) {
50         super(context, attrs);
51         final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ButtonBarLayout);
52         mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking, true);
53         ta.recycle();
54     }
55 
setAllowStacking(boolean allowStacking)56     public void setAllowStacking(boolean allowStacking) {
57         if (mAllowStacking != allowStacking) {
58             mAllowStacking = allowStacking;
59             if (!mAllowStacking && getOrientation() == LinearLayout.VERTICAL) {
60                 setStacked(false);
61             }
62             requestLayout();
63         }
64     }
65 
66     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)67     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
68         final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
69 
70         if (mAllowStacking) {
71             if (widthSize > mLastWidthSize && isStacked()) {
72                 // We're being measured wider this time, try un-stacking.
73                 setStacked(false);
74             }
75 
76             mLastWidthSize = widthSize;
77         }
78 
79         boolean needsRemeasure = false;
80 
81         // If we're not stacked, make sure the measure spec is AT_MOST rather
82         // than EXACTLY. This ensures that we'll still get TOO_SMALL so that we
83         // know to stack the buttons.
84         final int initialWidthMeasureSpec;
85         if (!isStacked() && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
86             initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
87 
88             // We'll need to remeasure again to fill excess space.
89             needsRemeasure = true;
90         } else {
91             initialWidthMeasureSpec = widthMeasureSpec;
92         }
93 
94         super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec);
95 
96         if (mAllowStacking && !isStacked()) {
97             final boolean stack;
98 
99             final int measuredWidth = getMeasuredWidthAndState();
100             final int measuredWidthState = measuredWidth & View.MEASURED_STATE_MASK;
101             stack = measuredWidthState == View.MEASURED_STATE_TOO_SMALL;
102 
103             if (stack) {
104                 setStacked(true);
105                 // Measure again in the new orientation.
106                 needsRemeasure = true;
107             }
108         }
109 
110         if (needsRemeasure) {
111             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
112         }
113 
114         // Compute minimum height such that, when stacked, some portion of the
115         // second button is visible.
116         int minHeight = 0;
117         final int firstVisible = getNextVisibleChildIndex(0);
118         if (firstVisible >= 0) {
119             final View firstButton = getChildAt(firstVisible);
120             final LayoutParams firstParams = (LayoutParams) firstButton.getLayoutParams();
121             minHeight += getPaddingTop() + firstButton.getMeasuredHeight()
122                     + firstParams.topMargin + firstParams.bottomMargin;
123             if (isStacked()) {
124                 final int secondVisible = getNextVisibleChildIndex(firstVisible + 1);
125                 if (secondVisible >= 0) {
126                     minHeight += getChildAt(secondVisible).getPaddingTop()
127                             + (int) (PEEK_BUTTON_DP * getResources().getDisplayMetrics().density);
128                 }
129             } else {
130                 minHeight += getPaddingBottom();
131             }
132         }
133 
134         if (ViewCompat.getMinimumHeight(this) != minHeight) {
135             setMinimumHeight(minHeight);
136         }
137     }
138 
getNextVisibleChildIndex(int index)139     private int getNextVisibleChildIndex(int index) {
140         for (int i = index, count = getChildCount(); i < count; i++) {
141             if (getChildAt(i).getVisibility() == View.VISIBLE) {
142                 return i;
143             }
144         }
145         return -1;
146     }
147 
148     @Override
getMinimumHeight()149     public int getMinimumHeight() {
150         return Math.max(mMinimumHeight, super.getMinimumHeight());
151     }
152 
setStacked(boolean stacked)153     private void setStacked(boolean stacked) {
154         setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
155         setGravity(stacked ? Gravity.RIGHT : Gravity.BOTTOM);
156 
157         final View spacer = findViewById(R.id.spacer);
158         if (spacer != null) {
159             spacer.setVisibility(stacked ? View.GONE : View.INVISIBLE);
160         }
161 
162         // Reverse the child order. This is specific to the Material button
163         // bar's layout XML and will probably not generalize.
164         final int childCount = getChildCount();
165         for (int i = childCount - 2; i >= 0; i--) {
166             bringChildToFront(getChildAt(i));
167         }
168     }
169 
isStacked()170     private boolean isStacked() {
171         return getOrientation() == LinearLayout.VERTICAL;
172     }
173 }
174