微调器下拉项目:宽度是如何确定的?

Spinner dropdown items: how is width determined?

我的应用程序中有一个 Spinner,带有自定义的下拉视图。这是下拉项的布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:focusable="false"
    android:orientation="horizontal">

    <androidx.appcompat.widget.AppCompatImageButton
        android:id="@+id/leadingButton"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_weight="0" />

    <FrameLayout
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_gravity="center_vertical"
        android:layout_weight="1">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:id="@+id/dropdown_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/dropdown_text_subtitle"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/dropdown_text" />
        </RelativeLayout>
    </FrameLayout>

    <androidx.appcompat.widget.AppCompatImageButton
        android:id="@+id/trailingButton"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_weight="0" />
</LinearLayout>

Android Studio 警告我,我的 FrameLayout 没用。但是当我取出 FrameLayout 时,下拉视图变窄,并且不再与微调器本身对齐。当我尝试用 ConstraintLayout 重写下拉项时,我遇到了同样的问题:下拉列表变窄,大约是 Spinner 大小的一半,并且无法显示所有文本,即使 ConstraintLayoutandroid:layout_width="match_parent".

草图来说明我的意思:

为什么会这样?我如何根据布局预测下拉菜单的宽度?

我发现这个下拉视图大小调整非常神奇

它告诉你 FrameLayout 是无用的,因为它只有一个子视图(相对布局)。

您的框架布局的宽度定义如下:

<FrameLayout
    android:layout_width="0dp"
    android:layout_weight="1"

您的相对布局的宽度定义为:

<RelativeLayout
    android:layout_width="match_parent"

因此,仅删除 FrameLayout 就意味着为宽度设置了不同的 "rule"。

要真正用 RelativeLayout 替换 FrameLayout,它应该如下所示:

<RelativeLayout
    android:layout_width="0dp"
    android:layout_weight="1"

你看过微调器的源代码吗class?我已经做了。这是我发现的(API 27 个来源):

微调器在内部使用 ListView(第一个 LOL),由 DropdownPopup(私有 class)支持:

private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {

在看它之前,先看一下 ListPopupWindow 因为有很多关于它必须处理的问题的信息。这是一个很大的 class 但在这些东西中,你可以看到:

    private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
    private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
    private int mDropDownHorizontalOffset;
    private int mDropDownVerticalOffset;

看起来 DropDown 是 - 默认情况下 - 基于基础 class 包装内容,但是,驱动的 DropDownPopup(并包含带有微调器中所有项目的适配器)也有一个 void computeContentWidth() {方法。

此方法是从 show() 方法调用的,因此在显示弹出窗口之前,每次都会发生此 计算

我认为这是您正在寻找的部分答案:

        void computeContentWidth() {
            final Drawable background = getBackground();
            int hOffset = 0;
            if (background != null) {
                background.getPadding(mTempRect);
                hOffset = isLayoutRtl() ? mTempRect.right : -mTempRect.left;
            } else {
                mTempRect.left = mTempRect.right = 0;
            }

            final int spinnerPaddingLeft = Spinner.this.getPaddingLeft();
            final int spinnerPaddingRight = Spinner.this.getPaddingRight();
            final int spinnerWidth = Spinner.this.getWidth();

            if (mDropDownWidth == WRAP_CONTENT) {
                int contentWidth =  measureContentWidth(
                        (SpinnerAdapter) mAdapter, getBackground());
                final int contentWidthLimit = mContext.getResources()
                        .getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right;
                if (contentWidth > contentWidthLimit) {
                    contentWidth = contentWidthLimit;
                }
                setContentWidth(Math.max(
                       contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
            } else if (mDropDownWidth == MATCH_PARENT) {
                setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
            } else {
                setContentWidth(mDropDownWidth);
            }

            if (isLayoutRtl()) {
                hOffset += spinnerWidth - spinnerPaddingRight - getWidth();
            } else {
                hOffset += spinnerPaddingLeft;
            }
            setHorizontalOffset(hOffset);
        }

您可能想在这里调试和设置断点,以观察这些值是什么以及它们的含义。

另一部分是 setContentWidth() 方法。此方法来自 ListPopupWindow,看起来像:

/**
     * Sets the width of the popup window by the size of its content. The final width may be
     * larger to accommodate styled window dressing.
     *
     * @param width Desired width of content in pixels.
     */
    public void setContentWidth(int width) {
        Drawable popupBackground = mPopup.getBackground();
        if (popupBackground != null) {
            popupBackground.getPadding(mTempRect);
            mDropDownWidth = mTempRect.left + mTempRect.right + width;
        } else {
            setWidth(width);
        }
    }

setWidth(也在那个class)它所做的只是:

    /**
     * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT}
     * or {@link #WRAP_CONTENT}.
     *
     * @param width Width of the popup window.
     */
    public void setWidth(int width) {
        mDropDownWidth = width;
    }

这个 mDropDownWidth 似乎到处都在使用,但也让我在 ListPopupWindow 中找到了这个其他方法...

    /**
     * Sets the width of the popup window by the size of its content. The final width may be
     * larger to accommodate styled window dressing.
     *
     * @param width Desired width of content in pixels.
     */
    public void setContentWidth(int width) {
        Drawable popupBackground = mPopup.getBackground();
        if (popupBackground != null) {
            popupBackground.getPadding(mTempRect);
            mDropDownWidth = mTempRect.left + mTempRect.right + width;
        } else {
            setWidth(width);
        }
    }

好了,需要更多逻辑,包括 "window dressing" (?)

我同意 Spinner 是一个设计糟糕的 class(或者更确切地说,设计过时),而且名字更是如此(在 2019 年 Google I/O,他们实际上在其中一个会议中解释了为什么名称 "Spinner" 提示:它来自第一个 android 原型)。通过查看所有这些代码,可能需要几个小时才能弄清楚微调器试图做什么以及它是如何工作的,但这次旅行不会愉快。

祝你好运。

我会重申我的建议,使用您说过您熟悉的 ConstraintLayout;至少,放弃重量。 通过查看它的工作原理(ListView!!!),权重计算保证了第二次 measure/layout 通过,这不仅效率极低且不需要,而且还可能导致内部数据适配器出现问题,这个 DropDown 事物管理所以显示 "list"。

最后还牵扯到另一个class,这都在一个PopupView中呈现。例如,PopupViews 是您打开菜单项时看到的内容,有时很难自定义,具体取决于您想要做什么。

为什么 Google 当时选择这种方法,我不知道,但它肯定需要更新,而且 Material 设计并没有给 table在这方面,因为它总是不完整或处于 alpha 状态,比其他任何东西都晚一年。