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