欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Kotlin 自定义 NavigationView

程序员文章站 2022-05-28 13:29:46
...

NavigationView 的布局写法如下

<android.support.design.widget.NavigationView
    android:id="@+id/nav_view"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:fitsSystemWindows="true"
    app:headerLayout="@layout/nav_header_main"
    app:menu="@menu/activity_main_drawer" />

查看NavigationView的源代码,发现左侧栏的布局是通过NavigationMenuPresenter这个类来实现的。源码的路径为: NavigationView -> NavigationMenuPresenter

public MenuView getMenuView(ViewGroup root) {
    if (this.menuView == null) {
        this.menuView = (NavigationMenuView)this.layoutInflater.inflate(layout.design_navigation_menu, root, false);
        if (this.adapter == null) {
            this.adapter = new NavigationMenuPresenter.NavigationMenuAdapter();
        }

        this.headerLayout = (LinearLayout)this.layoutInflater.inflate(layout.design_navigation_item_header, this.menuView, false);
        this.menuView.setAdapter(this.adapter);
    }

    return this.menuView;
}
public NavigationMenuPresenter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    switch(viewType) {
    case 0:
        return new NavigationMenuPresenter.NormalViewHolder(NavigationMenuPresenter.this.layoutInflater, parent, NavigationMenuPresenter.this.onClickListener);
    case 1:
        return new NavigationMenuPresenter.SubheaderViewHolder(NavigationMenuPresenter.this.layoutInflater, parent);
    case 2:
        return new NavigationMenuPresenter.SeparatorViewHolder(NavigationMenuPresenter.this.layoutInflater, parent);
    case 3:
        return new NavigationMenuPresenter.HeaderViewHolder(NavigationMenuPresenter.this.headerLayout);
    default:
        return null;
    }
}
private static class HeaderViewHolder extends NavigationMenuPresenter.ViewHolder {
    public HeaderViewHolder(View itemView) {
        super(itemView);
    }
}

private static class SeparatorViewHolder extends NavigationMenuPresenter.ViewHolder {
    public SeparatorViewHolder(LayoutInflater inflater, ViewGroup parent) {
        super(inflater.inflate(layout.design_navigation_item_separator, parent, false));
    }
}

private static class SubheaderViewHolder extends NavigationMenuPresenter.ViewHolder {
    public SubheaderViewHolder(LayoutInflater inflater, ViewGroup parent) {
        super(inflater.inflate(layout.design_navigation_item_subheader, parent, false));
    }
}

private static class NormalViewHolder extends NavigationMenuPresenter.ViewHolder {
    public NormalViewHolder(LayoutInflater inflater, ViewGroup parent, OnClickListener listener) {
        super(inflater.inflate(layout.design_navigation_item, parent, false));
        this.itemView.setOnClickListener(listener);
    }
}

资源文件的位置: sdk路径\extras\android\support\design\res\layout
(1)layout.design_navigation_item_header

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/navigation_header_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:paddingBottom="@dimen/design_navigation_separator_vertical_padding" />

(2)layout.design_navigation_item_subheader

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?attr/listPreferredItemHeightSmall"
    android:gravity="center_vertical|start"
    android:maxLines="1"
    android:paddingLeft="?attr/listPreferredItemPaddingLeft"
    android:paddingRight="?attr/listPreferredItemPaddingRight"
    android:textAppearance="@style/TextAppearance.AppCompat.Body2"
    android:textColor="?android:textColorSecondary"/>

(3)layout.design_navigation_item

<android.support.design.internal.NavigationMenuItemView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?attr/listPreferredItemHeightSmall"
    android:paddingLeft="?attr/listPreferredItemPaddingLeft"
    android:paddingRight="?attr/listPreferredItemPaddingRight"
    android:foreground="?attr/selectableItemBackground"
    android:focusable="true"/>

HORIZONTAL 和 VERTICAL都是 int 型的常量,定义在 LinearLayoutCompat.java 中:

  • HORIZONTAL = 0
  • VERTICAL = 1

因此下面的.setOrientation(0) 代表水平布局,而这个ViewStub 也之会出现在水平位置

public NavigationMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    this.accessibilityDelegate = new AccessibilityDelegateCompat() {
        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
            super.onInitializeAccessibilityNodeInfo(host, info);
            info.setCheckable(NavigationMenuItemView.this.checkable);
        }
    };
    this.setOrientation(0);
    LayoutInflater.from(context).inflate(layout.design_navigation_menu_item, this, true);
    this.iconSize = context.getResources().getDimensionPixelSize(dimen.design_navigation_icon_size);
    this.textView = (CheckedTextView)this.findViewById(id.design_menu_item_text);
    this.textView.setDuplicateParentStateEnabled(true);
    ViewCompat.setAccessibilityDelegate(this.textView, this.accessibilityDelegate);
}

layout.design_navigation_menu_item

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <CheckedTextView
            android:id="@+id/design_menu_item_text"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:drawablePadding="@dimen/design_navigation_icon_padding"
            android:gravity="center_vertical|start"
            android:maxLines="1"
            android:textAppearance="@style/TextAppearance.AppCompat.Body2"/>

    <ViewStub
            android:id="@+id/design_menu_item_action_area_stub"
            android:inflatedId="@+id/design_menu_item_action_area"
            android:layout="@layout/design_menu_item_action_area"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"/>

</merge>

layout.design_menu_item_action_area

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"/>

(4)layout.design_navigation_item_separator

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="?android:attr/listDivider"/>
</FrameLayout>

从下面代码打印的日志,我们可以得出一份大概的 NavigationView 结构图,如下

NavigationView
|
|--headerLayout
|
|--menu
    |
    |--HeaderViewHolder
    |  |
    |  |--LinearLayout
    |     |
    |     |--AppCompatImageView
    |     |--AppCompatTextView
    |     |--AppCompatTextView
    |
    |--SubheaderViewHolder
    |  |
    |  |--AppCompatTextView
    |
    |--NormalViewHolder
    |  |
    |  |--NavigationMenuItemView
    |     |
    |     |--AppCompatCheckedTextView
    |     |--ViewStub
    |
    |--SeparatorViewHolder
       |
       |--FrameLayout
          |
          |--View

在知道了官方的 NavigationView 的大致结构之后,我们就可以通过反射,来修改其中的一些属性的值了。

val context by lazy { this } //这里使用了委托,表示只有使用到context才会执行该段代码

nav_view.setBackgroundColor(ContextCompat.getColor(context, android.R.color.holo_red_light))
nav_view.setNavigationItemSelectedListener(this)
val presenterField = nav_view.javaClass.getDeclaredField("presenter")
presenterField.isAccessible = true
val menuPresenter = presenterField.get(nav_view)
val menuViewField = menuPresenter.javaClass.getDeclaredField("menuView")
menuViewField.isAccessible = true
val menuView = menuViewField.get(menuPresenter) as NavigationMenuView
menuView.addOnChildAttachStateChangeListener(object : RecyclerView.OnChildAttachStateChangeListener {
    override fun onChildViewAttachedToWindow(view: View) {
        val viewHolder = menuView.getChildViewHolder(view)
        Log.e("xxx1", viewHolder.javaClass.simpleName)
        Log.w("xxx2", viewHolder.itemView.javaClass.simpleName)
        //if(viewHolder != null && viewHolder.itemView != null)
        when (viewHolder.javaClass.simpleName) {
            /*
            HeaderViewHolder, LinearLayout, 1(LinearLayout, 3[AppCompatImageView, AppCompatTextView, AppCompatTextView])
            在 headerLayout 与 menu 之间,有且仅出现一次
            */
            "HeaderViewHolder" -> {
                if (viewHolder.itemView is LinearLayout) {
                    val layout = viewHolder.itemView as LinearLayout
                    layout.setBackgroundColor(ContextCompat.getColor(context, android.R.color.holo_blue_light))
                    Log.i("xxx3", layout.childCount.toString())
                    Log.d("xxx4", layout.getChildAt(0).javaClass.simpleName)
                    val child = layout.getChildAt(0) as LinearLayout
                    Log.v("xxx5", child.childCount.toString())
                    for (i in 0 until child.childCount) {
                        Log.v("xxx5", child.getChildAt(i).javaClass.simpleName)
                    }
                }
            }
            /*
            SubheaderViewHolder, AppCompatTextView
            在 item 作为容器的组中出现:<item><menu>……</menu></item>
             */
            "SubheaderViewHolder" -> {
                if (viewHolder.itemView is AppCompatTextView) {
                    val layout = viewHolder.itemView as AppCompatTextView
                    layout.setBackgroundColor(ContextCompat.getColor(context, android.R.color.holo_green_light))
                }
            }
            /*
            NormalViewHolder, NavigationMenuItemView, 2(AppCompatCheckedTextView, ViewStub)
            在 item 作为列表子项中出现
             */
            "NormalViewHolder" -> {
                if (viewHolder.itemView is NavigationMenuItemView) {
                    val layout = viewHolder.itemView as NavigationMenuItemView
                    layout.setBackgroundColor(ContextCompat.getColor(context, android.R.color.holo_orange_light))
                    Log.i("xxx3", layout.childCount.toString())
                    for (i in 0 until layout.childCount) {
                        Log.d("xxx4", layout.getChildAt(i).javaClass.simpleName)
                    }
                }
            }
            /*
            SeparatorViewHolder, FrameLayout, 1(View)
            仅在 SubheaderViewHolder 顶部出现,此外 SeparatorViewHolder 与其上留有一点空白,会透出 nav_view 的背景色
             */
            "SeparatorViewHolder" -> {
                if (viewHolder.itemView is FrameLayout) {
                    val layout = viewHolder.itemView as FrameLayout
                    val line = layout.getChildAt(0)
                    line.setBackgroundColor(ContextCompat.getColor(context, android.R.color.holo_purple))
                    line.layoutParams.height = 10
                    Log.i("xxx3", layout.childCount.toString())
                    Log.d("xxx4", layout.getChildAt(0).javaClass.simpleName)
                }
            }
            else -> {
                viewHolder.itemView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.darker_gray))
            }
        }
    }
    override fun onChildViewDetachedFromWindow(view: View) {}
})

Kotlin 自定义 NavigationView
Kotlin 自定义 NavigationView

通过实例化 ViewStub,并动态添加固件可以改变列表的布局。
注意:viewHolder.itemView 默认是水平方向的,你可以修改 viewHolder.itemView 的 orientation 属性,但是修改之后 icon 和 titile 便会消失,从而只剩通过 ViewStub 动态添加的布局

"NormalViewHolder" -> {
    if (viewHolder.itemView is NavigationMenuItemView) {
        val layout = viewHolder.itemView as NavigationMenuItemView
        layout.setBackgroundColor(ContextCompat.getColor(context, android.R.color.holo_orange_light))
        Log.i("xxx3", layout.childCount.toString())
        for (i in 0 until layout.childCount) {
            val child = layout.getChildAt(i)
            Log.d("xxx4", child.javaClass.simpleName)
            if(child is ViewStub) {
                val viewStub = child.inflate() as FrameLayout
                val textview = TextView(context)
                textview.text = "xx"
                val layoutParam = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
                layoutParam.gravity = GravityCompat.START
                viewStub.addView(textview, layoutParam)
            }
        }
    }
}

Kotlin 自定义 NavigationView