Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2e750a14 authored by Alan Viverette's avatar Alan Viverette
Browse files

Scroll alert dialog buttons vertically when there's not enough space

Bug: 25897915
Change-Id: I3cca89450728100375c6cf8ef9142c9a002fd8ba
parent 46125323
Loading
Loading
Loading
Loading
+8 −6
Original line number Diff line number Diff line
@@ -43,7 +43,6 @@ import android.view.ViewStub;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
@@ -115,6 +114,8 @@ public class AlertController {
    private int mSingleChoiceItemLayout;
    private int mListItemLayout;

    private boolean mShowTitle;

    private int mButtonPanelLayoutHint = AlertDialog.LAYOUT_HINT_NONE;

    private Handler mHandler;
@@ -150,7 +151,7 @@ public class AlertController {
        private WeakReference<DialogInterface> mDialog;

        public ButtonHandler(DialogInterface dialog) {
            mDialog = new WeakReference<DialogInterface>(dialog);
            mDialog = new WeakReference<>(dialog);
        }

        @Override
@@ -200,6 +201,7 @@ public class AlertController {
        mListItemLayout = a.getResourceId(
                R.styleable.AlertDialog_listItemLayout,
                R.layout.select_dialog_item);
        mShowTitle = a.getBoolean(R.styleable.AlertDialog_showTitle, true);

        a.recycle();
    }
@@ -600,21 +602,21 @@ public class AlertController {
    }

    private void setupTitle(ViewGroup topPanel) {
        if (mCustomTitleView != null) {
        if (mCustomTitleView != null && mShowTitle) {
            // Add the custom title view directly to the topPanel layout
            LayoutParams lp = new LayoutParams(
            final LayoutParams lp = new LayoutParams(
                    LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);

            topPanel.addView(mCustomTitleView, 0, lp);

            // Hide the title template
            View titleTemplate = mWindow.findViewById(R.id.title_template);
            final View titleTemplate = mWindow.findViewById(R.id.title_template);
            titleTemplate.setVisibility(View.GONE);
        } else {
            mIconView = (ImageView) mWindow.findViewById(R.id.icon);

            final boolean hasTextTitle = !TextUtils.isEmpty(mTitle);
            if (hasTextTitle) {
            if (hasTextTitle && mShowTitle) {
                // Display the title if a title is supplied, else hide it.
                mTitleView = (TextView) mWindow.findViewById(R.id.alertTitle);
                mTitleView.setText(mTitle);
+239 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.widget;

import android.annotation.AttrRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StyleRes;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import com.android.internal.R;

/**
 * Special implementation of linear layout that's capable of laying out alert
 * dialog components.
 * <p>
 * A dialog consists of up to three panels. All panels are optional, and a
 * dialog may contain only a single panel. The panels are laid out according
 * to the following guidelines:
 * <ul>
 *     <li>topPanel: exactly wrap_content</li>
 *     <li>contentPanel OR customPanel: at most fill_parent, first priority for
 *         extra space</li>
 *     <li>buttonPanel: at least minHeight, at most wrap_content, second
 *         priority for extra space</li>
 * </ul>
 */
public class AlertDialogLayout extends LinearLayout {

    public AlertDialogLayout(@Nullable Context context) {
        super(context);
    }

    public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs,
            @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs,
            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (!tryOnMeasure(widthMeasureSpec, heightMeasureSpec)) {
            // Failed to perform custom measurement, let superclass handle it.
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    private boolean tryOnMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        View topPanel = null;
        View buttonPanel = null;
        View middlePanel = null;

        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                continue;
            }

            final int id = child.getId();
            switch (id) {
                case R.id.topPanel:
                    topPanel = child;
                    break;
                case R.id.buttonPanel:
                    buttonPanel = child;
                    break;
                case R.id.contentPanel:
                case R.id.customPanel:
                    if (middlePanel != null) {
                        // Both the content and custom are visible. Abort!
                        return false;
                    }
                    middlePanel = child;
                    break;
                default:
                    // Unknown top-level child. Abort!
                    return false;
            }
        }

        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // Treat all panel widths as MATCH_PARENT
        // by translating AT_MOST to EXACTLY.
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        if (widthMode == MeasureSpec.AT_MOST) {
            final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
        }

        int childState = 0;
        int usedHeight = getPaddingTop() + getPaddingBottom();

        if (topPanel != null) {
            topPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);

            usedHeight += topPanel.getMeasuredHeight();
            childState = combineMeasuredStates(childState, topPanel.getMeasuredState());
        }

        int buttonHeight = 0;
        int buttonWantsHeight = 0;
        if (buttonPanel != null) {
            buttonPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);
            buttonHeight = resolveMinimumHeight(buttonPanel);
            buttonWantsHeight = buttonPanel.getMeasuredHeight() - buttonHeight;

            usedHeight += buttonHeight;
            childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState());
        }

        int middleHeight = 0;
        if (middlePanel != null) {
            final int childHeightSpec;
            if (heightMode == MeasureSpec.UNSPECIFIED) {
                childHeightSpec = MeasureSpec.UNSPECIFIED;
            } else {
                childHeightSpec = MeasureSpec.makeMeasureSpec(
                        Math.max(0, heightSize - usedHeight), heightMode);
            }

            middlePanel.measure(widthMeasureSpec, childHeightSpec);
            middleHeight = middlePanel.getMeasuredHeight();

            usedHeight += middleHeight;
            childState = combineMeasuredStates(childState, middlePanel.getMeasuredState());
        }

        int remainingHeight = heightSize - usedHeight;

        // Time for the "real" button measure pass. If we have remaining space,
        // make the button pane bigger up to its target height. Otherwise,
        // just remeasure the button at whatever height it needs.
        if (buttonPanel != null) {
            usedHeight -= buttonHeight;

            final int heightToGive = Math.min(remainingHeight, buttonWantsHeight);
            if (heightToGive > 0) {
                remainingHeight -= heightToGive;
                buttonHeight += heightToGive;
            }

            final int childHeightSpec = MeasureSpec.makeMeasureSpec(
                    buttonHeight, MeasureSpec.EXACTLY);
            buttonPanel.measure(widthMeasureSpec, childHeightSpec);

            usedHeight += buttonPanel.getMeasuredHeight();
            childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState());
        }

        // If we still have remaining space, make the middle pane bigger up
        // to the maximum height.
        if (middlePanel != null && remainingHeight > 0) {
            usedHeight -= middleHeight;

            final int heightToGive = remainingHeight;
            remainingHeight -= heightToGive;
            middleHeight += heightToGive;

            // Pass the same height mode as we're using for the dialog itself.
            // If it's EXACTLY, then the middle pane MUST use the entire
            // height.
            final int childHeightSpec = MeasureSpec.makeMeasureSpec(
                    middleHeight, heightMode);
            middlePanel.measure(widthMeasureSpec, childHeightSpec);

            usedHeight += middlePanel.getMeasuredHeight();
            childState = combineMeasuredStates(childState, middlePanel.getMeasuredState());
        }

        // Compute desired width as maximum child width.
        int maxWidth = 0;
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
            }
        }
        maxWidth += getPaddingLeft() + getPaddingRight();

        final int widthSizeAndState = resolveSizeAndState(maxWidth, widthMeasureSpec, childState);
        final int heightSizeAndState = resolveSizeAndState(usedHeight, heightMeasureSpec, 0);
        setMeasuredDimension(widthSizeAndState, heightSizeAndState);
        return true;
    }

    /**
     * Attempts to resolve the minimum height of a view.
     * <p>
     * If the view doesn't have a minimum height set and only contains a single
     * child, attempts to resolve the minimum height of the child view.
     *
     * @param v the view whose minimum height to resolve
     * @return the minimum height
     */
    private int resolveMinimumHeight(View v) {
        final int minHeight = v.getMinimumHeight();
        if (minHeight > 0) {
            return minHeight;
        }

        if (v instanceof ViewGroup) {
            final ViewGroup vg = (ViewGroup) v;
            if (vg.getChildCount() == 1) {
                return resolveMinimumHeight(vg.getChildAt(0));
            }
        }

        return 0;
    }
}
+44 −2
Original line number Diff line number Diff line
@@ -30,15 +30,19 @@ import com.android.internal.R;
 * orientation when it can't fit its child views horizontally.
 */
public class ButtonBarLayout extends LinearLayout {
    // Whether to allow vertically stacked button bars. This is disabled for
    // configurations with a small (e.g. less than 320dp) screen height. -->
    /** Minimum screen height required for button stacking. */
    private static final int ALLOW_STACKING_MIN_HEIGHT_DP = 320;

    /** Amount of the second button to "peek" above the fold when stacked. */
    private static final int PEEK_BUTTON_DP = 16;

    /** Whether the current configuration allows stacking. */
    private boolean mAllowStacking;

    private int mLastWidthSize = -1;

    private int mMinimumHeight = 0;

    public ButtonBarLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

@@ -105,6 +109,44 @@ public class ButtonBarLayout extends LinearLayout {
        if (needsRemeasure) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }

        // Compute minimum height such that, when stacked, some portion of the
        // second button is visible.
        int minHeight = 0;
        final int firstVisible = getNextVisibleChildIndex(0);
        if (firstVisible >= 0) {
            final View firstButton = getChildAt(firstVisible);
            final LayoutParams firstParams = (LayoutParams) firstButton.getLayoutParams();
            minHeight += getPaddingTop() + firstButton.getMeasuredHeight()
                    + firstParams.topMargin + firstParams.bottomMargin;
            if (isStacked()) {
                final int secondVisible = getNextVisibleChildIndex(firstVisible + 1);
                if (secondVisible >= 0) {
                    minHeight += getChildAt(secondVisible).getPaddingTop()
                            + PEEK_BUTTON_DP * getResources().getDisplayMetrics().density;
                }
            } else {
                minHeight += getPaddingBottom();
            }
        }

        if (getMinimumHeight() != minHeight) {
            setMinimumHeight(minHeight);
        }
    }

    private int getNextVisibleChildIndex(int index) {
        for (int i = index, count = getChildCount(); i < count; i++) {
            if (getChildAt(i).getVisibility() == View.VISIBLE) {
                return i;
            }
        }
        return -1;
    }

    @Override
    public int getMinimumHeight() {
        return Math.max(mMinimumHeight, super.getMinimumHeight());
    }

    private void setStacked(boolean stacked) {
+45 −39
Original line number Diff line number Diff line
@@ -15,9 +15,15 @@
     limitations under the License.
-->

<com.android.internal.widget.ButtonBarLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/buttonPanel"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:scrollbarAlwaysDrawVerticalTrack="true"
            android:scrollIndicators="top|bottom"
            android:fillViewport="true"
            style="?attr/buttonBarStyle">
    <com.android.internal.widget.ButtonBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layoutDirection="locale"
@@ -26,8 +32,7 @@
        android:paddingEnd="12dp"
        android:paddingTop="4dp"
        android:paddingBottom="4dp"
    android:gravity="bottom"
    style="?attr/buttonBarStyle">
        android:gravity="bottom">

        <Button
            android:id="@+id/button3"
@@ -54,3 +59,4 @@
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </com.android.internal.widget.ButtonBarLayout>
</ScrollView>
+6 −5
Original line number Diff line number Diff line
@@ -15,7 +15,7 @@
     limitations under the License.
-->

<LinearLayout
<com.android.internal.widget.AlertDialogLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parentPanel"
    android:layout_width="match_parent"
@@ -28,7 +28,6 @@
        android:id="@+id/contentPanel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:minHeight="48dp">

        <ScrollView
@@ -64,7 +63,6 @@
        android:id="@+id/customPanel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:minHeight="48dp">

        <FrameLayout
@@ -73,5 +71,8 @@
            android:layout_height="wrap_content" />
    </FrameLayout>

    <include layout="@layout/alert_dialog_button_bar_material" />
</LinearLayout>
    <include
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        layout="@layout/alert_dialog_button_bar_material" />
</com.android.internal.widget.AlertDialogLayout>
Loading