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