<com.google.android.material.textfield.TextInputLayout
            android:id="@+id/input_host"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/dialog_item_logo_txt_padding"
            android:layout_toRightOf="@+id/iv_host_logo"
            android:textColorHint="@color/item_divider_dark_gray"
            app:boxBackgroundColor="#00000000"
            app:hintTextAppearance="@style/OurTextInputHint"
            app:boxStrokeColor="@color/blue_gray_state_color">
            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_host"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:hint="@string/ID_BOOK_INFO_HOST"
                android:minWidth="@dimen/book_edit_text_width"
                android:singleLine="true"
                android:text="x"
                android:imeOptions="actionDone"
                android:textColor="@color/dialog_item_text_color"
                android:textSize="20sp" />
        </com.google.android.material.textfield.TextInputLayout>

上边那个hint文字的颜色设置textColorHint 这个是state状态的,选中状态可以变个深色

app:hintTextAppearance :hint的文字其他属性,比如大小,只能通过这个设置了

    <style name="OurTextInputHint" parent="TextAppearance.Design.Hint">
        <item name="android:textSize">16sp</item>
    </style>

可以看下源码,Layout的默认属性是这个,点进去看这个style就知道有啥了
private static final int DEF_STYLE_RES = R.style.Widget_Design_TextInputLayout;

  <style name="Widget.Design.TextInputLayout" parent="android:Widget">
    <item name="materialThemeOverlay">@style/ThemeOverlay.Design.TextInputEditText</item>
    <item name="enforceMaterialTheme">false</item>
    <item name="enforceTextAppearance">false</item>
    <item name="boxBackgroundMode">none</item>
    <item name="boxStrokeColor">@color/design_box_stroke_color</item>
    <item name="passwordToggleDrawable">@drawable/design_password_eye</item>
    <item name="passwordToggleTint">@color/design_icon_tint</item>
    <item name="passwordToggleContentDescription">@string/password_toggle_content_description</item>
    <item name="endIconTint">@color/design_icon_tint</item>
    <item name="startIconTint">@color/design_icon_tint</item>
    <item name="counterTextAppearance">@style/TextAppearance.Design.Counter</item>
    <item name="counterOverflowTextAppearance">@style/TextAppearance.Design.Counter.Overflow</item>
    <item name="errorTextAppearance">@style/TextAppearance.Design.Error</item>
    <item name="helperTextTextAppearance">@style/TextAppearance.Design.HelperText</item>
    <item name="hintTextAppearance">@style/TextAppearance.Design.Hint</item>
    <item name="counterTextColor">@null</item>
    <item name="counterOverflowTextColor">@null</item>
    <item name="errorTextColor">@null</item>
    <item name="helperTextTextColor">@null</item>
    <item name="hintTextColor">@null</item>
    <item name="shapeAppearance">@null</item>
    <item name="shapeAppearanceOverlay">@null</item>
  </style>

TextInputEditText

这个其实没太多的东西,就是和TextInputLayout交互下hint的值而已。
如果它自己没有设置hint,那就取容器也就是TextInputLayout的hint值。

    @Override
  public CharSequence getHint() {
    // Certain test frameworks expect the actionable element to expose its hint as a label. When
    // TextInputLayout is providing our hint, retrieve it from the parent layout.
    TextInputLayout layout = getTextInputLayout();
    if ((layout != null) && layout.isProvidingHint()) {
      return layout.getHint();
    return super.getHint();
  @Override
  public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
    final InputConnection ic = super.onCreateInputConnection(outAttrs);
    if (ic != null && outAttrs.hintText == null) {
      // If we don't have a hint and our parent is a TextInputLayout, use its hint for the
      // EditorInfo. This allows us to display a hint in 'extract mode'.
      outAttrs.hintText = getHintFromLayout();
    return ic;
  @Nullable
  private TextInputLayout getTextInputLayout() {
    ViewParent parent = getParent();
    while (parent instanceof View) {
      if (parent instanceof TextInputLayout) {
        return (TextInputLayout) parent;
      parent = parent.getParent();
    return null;
  @Nullable
  private CharSequence getHintFromLayout() {
    TextInputLayout layout = getTextInputLayout();
    return (layout != null) ? layout.getHint() : null;

先看一些参数的效果

  • app:boxBackgroundColor , boxBackgroundMode
  •             <com.google.android.material.textfield.TextInputLayout
                    app:boxBackgroundMode="filled">
                <com.google.android.material.textfield.TextInputLayout
                    android:background="@mipmap/adp_logo"
                    app:boxBackgroundColor="#E91E63"
                    app:boxBackgroundMode="outline">
    

    boxBackgroundMode:3种,默认的filled,还有outline以及none
    为filled,有个灰色背景色,edittext 点击以后,下边是一条线,主题色
    而且好像上边2个角是圆角
    app:boxBackgroundColor 可以修改背景色。
    android:background :会被boxBackgroundColor的颜色挡住,可能看不见

                    app:boxCornerRadiusBottomEnd="10dp"
                    app:boxCornerRadiusBottomStart="10dp"
                    app:boxCornerRadiusTopStart="10dp"
                    app:boxCornerRadiusTopEnd="10dp"
    
  • 选中以后底部那条线【filled模式】,或者边框的颜色[outline模式]
  • app:boxStrokeColor="#009688"
    

    strokeColor是支持state的,也就是聚焦和非聚焦不同的颜色,上边那种直接设置个颜色,那么就是foucus的颜色拉。
    //boxStrokeColor 如果是单独的色值,非isStateful的情况,就走的else,也就是给了focusColor
    //如果有state的话,对应设置下if里的3种state颜色就可以拉。

        ColorStateList boxStrokeColorStateList =
            MaterialResources.getColorStateList(context, a, R.styleable.TextInputLayout_boxStrokeColor);
        if (boxStrokeColorStateList != null && boxStrokeColorStateList.isStateful()) {
          defaultStrokeColor = boxStrokeColorStateList.getDefaultColor();
          disabledColor =
              boxStrokeColorStateList.getColorForState(new int[] {-android.R.attr.state_enabled}, -1);
          hoveredStrokeColor =
              boxStrokeColorStateList.getColorForState(new int[] {android.R.attr.state_hovered}, -1);
          focusedStrokeColor =
              boxStrokeColorStateList.getColorForState(new int[] {android.R.attr.state_focused}, -1);
        } else {
          // If attribute boxStrokeColor is not a color state list but only a single value, its value
          // will be applied to the box's focus state.
          focusedStrokeColor =
              a.getColor(R.styleable.TextInputLayout_boxStrokeColor, Color.TRANSPARENT);
          defaultStrokeColor =
              ContextCompat.getColor(context, R.color.mtrl_textinput_default_box_stroke_color);
          disabledColor = ContextCompat.getColor(context, R.color.mtrl_textinput_disabled_color);
          hoveredStrokeColor =
              ContextCompat.getColor(context, R.color.mtrl_textinput_hovered_box_stroke_color);
    

    app:boxStrokeWidth="5dp" 看到有个属性,设置了下,感觉没效果

  • hint文字
    正确的合理做法是随便哪个其中一个设置hint就行
    Layout和EditText都设置了,那么就各是各的,如下图,
  •             <com.google.android.material.textfield.TextInputLayout
                    android:hint="hint layout">
                    <com.google.android.material.textfield.TextInputEditText
                        android:textColorHint="#FF5722"
                 android:hint="hint">
    
        <style name="hintText" parent="Widget.Design.TextInputLayout">
            <item name="android:textSize">16sp</item>
        </style>
    
  • helperText 相关
  •                 app:helperText="helper text"
                    app:helperTextTextColor="#009688"
                    app:helperTextEnabled="true"
                    app:helperTextTextAppearance="@style/test"
    

    默认的属性,enalbe默认也是true的

    <item name="helperTextTextAppearance">@style/TextAppearance.Design.HelperText</item>
    <item name="helperTextTextColor">@null</item>
    

    效果图,java代码里也有对应的方法可以处理

                    app:counterEnabled="true"
                    app:counterMaxLength="20"
                    app:counterTextColor="@color/colorAccent"
                    app:counterOverflowTextColor="@color/txt_black_white"
    app:counterTextAppearance="@style/test"//字体还可以设置style,其他类似,后边不在说这种
    

    默认是不可用的,2种颜色,
    counterMaxLength的值来决定,
    比这个大的就是overflow颜色,否则就是正常的app:counterTextColor
    注意事项,这2个color都不能直接使用#000000这种,必须是一个color资源也就是@color/xxx
    主要就是看mode区分作用的,
    none:默认的,就是不显示icon,你设置了也没用。It will not display an end icon
    clear_text :点击一下文字清除
    password_toggle: 密码显示明文与否的开关
    custom: icon会显示,就是没点击效果而已。

                    app:endIconDrawable="@mipmap/book_add"
                    app:endIconMode="clear_text"
    

    注意事项:password_toggle的时候,你里边的EditText的inputType需要是password才行
    其实了,TextInputLayout前后两个icon都是可以点击的,而且可以有状态的
    如下,Checkable设置为true

    app:startIconCheckable="true"
    app:startIconDrawable="@drawable/design_password_eye"
    

    代码里就可以用了

            et_layout2.setStartIconOnClickListener {
                println("it=====$it")//listener里返回的view是CheckableImageButton【不对外使用的】
                if(it is Checkable){
                    println("check=======${it.isChecked}")
                        // Store the current cursor position
                        val selection = editText!!.selectionEnd
    //显示隐藏密码
                        this.editText?.transformationMethod = if(it.isChecked) PasswordTransformationMethod.getInstance() else  null
                        // And restore the cursor position
                        editText!!.setSelection(selection)
                    it.toggle()//因为是checkable的,所以我们可以切换状态,也可以拿到当前状态来做处理,比如隐藏密码
    
    public class TextInputLayout extends LinearLayout{
    public TextInputLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        setOrientation(VERTICAL);//垂直方向的线性布局
        inputFrame = new FrameLayout(context);
        inputFrame.setAddStatesFromChildren(true);
        addView(inputFrame);//先加了一个帧布局进来
        startLayout = new LinearLayout(context);
        startLayout.setOrientation(HORIZONTAL);
        startLayout.setLayoutParams(
            new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.MATCH_PARENT,
                Gravity.START | Gravity.LEFT));
        inputFrame.addView(startLayout);//frame里左侧添加一个startLayout
        endLayout = new LinearLayout(context);
        endLayout.setOrientation(HORIZONTAL);
        endLayout.setLayoutParams(
            new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.MATCH_PARENT,
                Gravity.END | Gravity.RIGHT));
        inputFrame.addView(endLayout);//frame里右侧添加一个endLayout
        endIconFrame = new FrameLayout(context);
        endIconFrame.setLayoutParams(
            new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
    //读取attr属性
        // Initialize error icon view.
        errorIconView =
            (CheckableImageButton)
                LayoutInflater.from(getContext())
                    .inflate(R.layout.design_text_input_end_icon, endLayout, false);
        errorIconView.setVisibility(GONE);
        // Initialize start icon view.
        startIconView =
            (CheckableImageButton)
                LayoutInflater.from(getContext())
                    .inflate(R.layout.design_text_input_start_icon, startLayout, false);
        startIconView.setVisibility(GONE);
        // Initialize end icon view.
        endIconView =
            (CheckableImageButton)
                LayoutInflater.from(getContext())
                    .inflate(R.layout.design_text_input_end_icon, endIconFrame, false);
        endIconFrame.addView(endIconView);
        // Set up prefix view.
        prefixTextView = new AppCompatTextView(context);
        prefixTextView.setId(R.id.textinput_prefix_text);
        prefixTextView.setLayoutParams(
            new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
        startLayout.addView(startIconView);
        startLayout.addView(prefixTextView);
        // Set up suffix view.
        suffixTextView = new AppCompatTextView(context);
        suffixTextView.setId(R.id.textinput_suffix_text);
        suffixTextView.setLayoutParams(
            new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT,
                Gravity.BOTTOM));
        ViewCompat.setAccessibilityLiveRegion(
            suffixTextView, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
        endLayout.addView(suffixTextView);
        endLayout.addView(errorIconView);
        endLayout.addView(endIconFrame);
    //找到里边的EditText然后放到我们new出来的帧布局里
      public void addView(
          @NonNull View child, int index, @NonNull final ViewGroup.LayoutParams params) {
        if (child instanceof EditText) {
          // Make sure that the EditText is vertically at the bottom, so that it sits on the
          // EditText's underline
          FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(params);
          flp.gravity = Gravity.CENTER_VERTICAL | (flp.gravity & ~Gravity.VERTICAL_GRAVITY_MASK);
          inputFrame.addView(child, flp);
          // Now use the EditText's LayoutParams as our own and update them to make enough space
          // for the label
          inputFrame.setLayoutParams(params);
          updateInputLayoutMargins();
          setEditText((EditText) child);
        } else {
          // Carry on adding the View...
          super.addView(child, index, params);
    

    hint部分简单看下源码
    app:boxBackgroundMode="outline"
    private boolean cutoutEnabled() { return hintEnabled && !TextUtils.isEmpty(hint) && boxBackground instanceof CutoutDrawable; private void openCutout() { if (!cutoutEnabled()) { return; final RectF cutoutBounds = tmpRectF; collapsingTextHelper.getCollapsedTextActualBounds(cutoutBounds); applyCutoutPadding(cutoutBounds); // Offset the cutout bounds by the TextInputLayout's left padding to ensure that the cutout is // inset relative to the TextInputLayout's bounds. cutoutBounds.offset(-getPaddingLeft(), 0); //可以看到background被减掉一部分,就是显示展开的hint的那部分 ((CutoutDrawable) boxBackground).setCutout(cutoutBounds); //通过helper获取rect public void getCollapsedTextActualBounds(@NonNull RectF bounds) { boolean isRtl = calculateIsRtl(text); bounds.left = !isRtl ? collapsedBounds.left : collapsedBounds.right - calculateCollapsedTextWidth(); bounds.top = collapsedBounds.top; bounds.right = !isRtl ? bounds.left + calculateCollapsedTextWidth() : collapsedBounds.right; bounds.bottom = collapsedBounds.top + getCollapsedTextHeight();

    看下代码的初始化,肯定是CutoutDrawable的

          case BOX_BACKGROUND_OUTLINE:
            if (hintEnabled && !(boxBackground instanceof CutoutDrawable)) {
              boxBackground = new CutoutDrawable(shapeAppearanceModel);
            } else {
              boxBackground = new MaterialShapeDrawable(shapeAppearanceModel);
            boxUnderline = null;
            break;
    

    现在的studio有个layout inspector,我们可以用这个来分析这个view的结构

    分两部分,上边的 startIcon,prefix text, editText,suffix text,endIcon在一个FrameLayout里
    下边的helper,error,count之类的在下边的一个线性布局里[水平方向的,所以help或者error 文本太长的话可能把counter挤到屏幕外看不见了]

    刚开始想到的是把那个endIcon的gravity设置成bottom,结果发现不太好看,理想的应该是和文字居中对齐的。
    然后又继续去看源码,仔细的看布局里margin,高度咋设置的,首先看了edittextView的容器FrameLayout,在hint不可用的时候,它的margin已经是0了

      private int calculateLabelMarginTop() {
        if (!hintEnabled) {
          return 0;
        switch (boxBackgroundMode) {
          case BOX_BACKGROUND_OUTLINE:
            return (int) (collapsingTextHelper.getCollapsedTextHeight() / 2);
          case BOX_BACKGROUND_FILLED:
          case BOX_BACKGROUND_NONE:
            return (int) collapsingTextHelper.getCollapsedTextHeight();
          default:
            return 0;
    

    然后我用studio的工具layout inspect查看了下布局,发现EditTextView竟然有padding,4个方向都有,可我们xml里并没有给他设置.
    我尝试在xml手动设置padding为0,然后发现 EditTextView高度果然变小了,既然不是我们设置的,那么必然是默认属性里的

      </style>
        <style name="ThemeOverlay.MaterialComponents.TextInputEditText.FilledBox">
        <item name="editTextStyle">@style/Widget.MaterialComponents.TextInputEditText.FilledBox</item>
      </style>
        <style name="ThemeOverlay.MaterialComponents.TextInputEditText.FilledBox.Dense">
        <item name="editTextStyle">@style/Widget.MaterialComponents.TextInputEditText.FilledBox.Dense
        </item>
      </style>
        <style name="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox">
        <item name="editTextStyle">@style/Widget.MaterialComponents.TextInputEditText.OutlinedBox</item>
      </style>
        <style name="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox.Dense">
        <item name="editTextStyle">@style/Widget.MaterialComponents.TextInputEditText.OutlinedBox.Dense
        </item>
      </style>
    
        <style name="Widget.MaterialComponents.TextInputEditText.FilledBox" parent="Base.Widget.MaterialComponents.TextInputEditText">
        <item name="android:paddingTop">28dp</item>
        <item name="android:paddingBottom">12dp</item>
      </style>
        <style name="Widget.MaterialComponents.TextInputEditText.FilledBox.Dense">
        <item name="android:paddingTop">24dp</item>
        <item name="android:paddingBottom">8dp</item>
      </style>
        <style name="Widget.MaterialComponents.TextInputEditText.OutlinedBox" parent="Base.Widget.MaterialComponents.TextInputEditText"/>
        <style name="Widget.MaterialComponents.TextInputEditText.OutlinedBox.Dense">
        <item name="android:paddingTop">13dp</item>
        <item name="android:paddingBottom">13dp</item>
      </style>