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