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