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