【Android Jetpack】NavigationMenuView的简单封装

1. 前言

现需要做一个目录的页面,虽然可以使用另起一个Activity来解决,但是却做不出那种类似抽屉导航栏的效果。而这个效果刚好在番茄小说这个APP中存在,平时也在使用。

而百度了一下,发现大家所使用的都是Dialog来包装一个Activity。所以接下来看看在番茄小说中是如何实现的。

考虑了一下,直接自定义View控件即可,且考虑将其封装为一个第三方控件。

2. NavigationView学习

这个感觉封装的很好,至少站在使用的角度上来看是这样。传入headerLayoutmenu就可以加载出对应的界面布局。所以在考虑封装之前有必要了解和学习这个控件。比如常见的使用如下:

<com.google.android.material.navigation.NavigationView
    android:id="@+id/nav_view"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="end"
    android:fitsSystemWindows="true"
    app:headerLayout="@layout/nav_header"
    app:menu="@menu/nav_item" />

app:headerLayout="@layout/nav_header"app:menu="@menu/nav_item"分别对应各自的xml文件。

2.1 结构

public class NavigationView extends ScrimInsetsFrameLayout
public class ScrimInsetsFrameLayout extends FrameLayout

也就是说NavigationView其实是一个帧布局。在构造函数中,首先添加一个容器MenuView

this.addView((View)this.presenter.getMenuView(this));

然后将上面app:menuapp:headerLayout两个属性都加载读取出来:

// NavigationView 
if (a.hasValue(styleable.NavigationView_menu)) {
    
    
    this.inflateMenu(a.getResourceId(styleable.NavigationView_menu, 0));
}

if (a.hasValue(styleable.NavigationView_headerLayout)) {
    
    
    this.inflateHeaderView(a.getResourceId(styleable.NavigationView_headerLayout, 0));
}

对于menu

// NavigationMenu
public void inflateMenu(int resId) {
    
    
    this.presenter.setUpdateSuspended(true);
    this.getMenuInflater().inflate(resId, this.menu);
    this.presenter.setUpdateSuspended(false);
    this.presenter.updateMenuView(false);
}

// 继承实现关系
public class NavigationMenu extends MenuBuilder 
public class MenuBuilder implements SupportMenu
public interface SupportMenu extends Menu

NavigationMenu中提供了生成MenuItem的方法。对于HeaderLayout的布局文件,实例化为:

// NavigationView 
public View inflateHeaderView(@LayoutRes int res) {
    
    
    return this.presenter.inflateHeaderView(res);
}

private final NavigationMenuPresenter presenter;

// NavigationMenuPresenter
public View inflateHeaderView(@LayoutRes int res) {
    
    
    View view = this.layoutInflater.inflate(res, this.headerLayout, false);
    this.addHeaderView(view);
    return view;
}

public void addHeaderView(@NonNull View view) {
    
    
    this.headerLayout.addView(view);
    this.menuView.setPadding(0, 0, 0, this.menuView.getPaddingBottom());
}

注意到在实例化的时候并没有对返回的view进行处理,所以将实例化出来的view添加到了NavigationMenuPresenter中。headerLayout定义为一个LinearLayout

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

父控件menuView为一个自定义控件NavigationMenuView,继承自RecycleView,并且实现了MenuView接口。

3. 思路

考虑到NavigationMenuView可以方便的传入item.xml文件,且不用处理点击效果。决定就使用这个控件来作为打开的小说目录,因为方便简单。那么只需要做的就是:

  • 自定义一个ViewGroup继承自FrameLayout,然后配置其layout即可。
  • 暴露出打开NavigationMenuView和关闭的方法,然后使用属性动画,添加一个简单的动画效果即可。
  • NavigationMenuViewitem点击事件添加监听,并返回其每个menuItemid
  • 处理Gravitystartend的时候的进入和退出动画。

效果:

在这里插入图片描述

4. 代码:

class HorizontalPopupLayout : BaseHorizontalPopupLayout {
    
    
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    private var mPopupViewWidth: Int = 0
    private var mNavigationView: NavigationView? = null
    private var mIsOpenViewFlag: Boolean = false
    private var mContentInLeft = false
    private var onClickListener: IHorizontalPopupLayoutItemClickListener? = null

    /**
     * 设置menuItem点击的监听接口
     */
    fun setOnItemClickListener(l: IHorizontalPopupLayoutItemClickListener) {
    
    
        this.onClickListener = l
    }

    @SuppressLint("RtlHardcoded", "UseCompatLoadingForColorStateLists")
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    
    
        super.onLayout(changed, left, top, right, bottom)
        if (childCount != 2) {
    
    
            throw Exception("在{
     
     $TAG}布局文件中,中必须有两个直接子元素!")
        }
        for (index in 0..childCount) {
    
    
            val child = getChildAt(index)
            if (child != null) {
    
    
                if (child is NavigationView) {
    
    
                    mPopupViewWidth = child.measuredWidth
                    try {
    
    
                        mNavigationView = getChildAt(index) as NavigationView
                    } catch (e: java.lang.Exception) {
    
    
                        throw Exception("在{
     
     $TAG}布局文件中,中第二个直接子元素必须为:NavigationView!")
                    }
                    when ((mNavigationView!!.layoutParams as FrameLayout.LayoutParams).gravity) {
    
    
                        Gravity.START, Gravity.LEFT -> {
    
    
                            child.layout(-mPopupViewWidth, 0, 0, measuredHeight)
                            mContentInLeft = true
                        }
                        Gravity.END, Gravity.RIGHT -> {
    
    
                            child.layout(
                                measuredWidth,
                                0,
                                measuredWidth + mPopupViewWidth,
                                measuredHeight
                            )
                            mContentInLeft = false
                        }
                        else -> {
    
    
                            child.layout(-mPopupViewWidth, 0, 0, measuredHeight)
                            mContentInLeft = true
                        }
                    }
                    mNavigationView!!.setNavigationItemSelectedListener(listener)
                } else {
    
    
                    child.layout(0, 0, measuredWidth, measuredHeight)
                }
            }
        }
        if (mNavigationView == null || !(mNavigationView is NavigationView)) {
    
    
            throw Exception("在{
     
     $TAG}布局文件中,第二个直接子元素必须是NavigationView!")
        }
    }


    private val listener = object : NavigationView.OnNavigationItemSelectedListener {
    
    
        override fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
    
    
            onClickListener?.onItemClick(menuItem.itemId)
            closeView()
            return true
        }
    }

    override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
    
    
        when (event?.action) {
    
    
            MotionEvent.ACTION_DOWN -> {
    
    
                return isCloseNavigationView(event.x)
            }
        }
        return super.onInterceptTouchEvent(event)
    }

    private fun isCloseNavigationView(x: Float): Boolean {
    
    
        mNavigationView?.let {
    
    
            if ((x < (measuredWidth - it.measuredWidth) && mIsOpenViewFlag && !mContentInLeft)
                || ((x > it.measuredWidth && mIsOpenViewFlag && mContentInLeft))
            ) {
    
    
                closeView()
                return true
            }
        }
        return false
    }

    override fun closeView() {
    
    
        if (!mIsOpenViewFlag) return
        mIsOpenViewFlag = false
        if (mContentInLeft) {
    
    
            animation(-mPopupViewWidth, CLOSE_ANIMATION_TIME, AccelerateDecelerateInterpolator())
        } else {
    
    
            animation(mPopupViewWidth, CLOSE_ANIMATION_TIME, AccelerateDecelerateInterpolator())
        }
    }

    override fun openView() {
    
    
        if (mIsOpenViewFlag) return
        mIsOpenViewFlag = true
        if (mContentInLeft) {
    
    
            animation(mPopupViewWidth, OPEN_ANIMATION_TIME, LinearInterpolator())
        } else {
    
    
            animation(-mPopupViewWidth, OPEN_ANIMATION_TIME, LinearInterpolator())
        }
    }

    @SuppressLint("ObjectAnimatorBinding", "Recycle")
    private fun animation(distance: Int, time: Long, interpolator: Interpolator) {
    
    
        mNavigationView?.apply {
    
    
            val animator = ObjectAnimator.ofFloat(
                mNavigationView,
                "translationX",
                1f * distance
            )
            animator.duration = time
            animator.interpolator = interpolator
            animator.start()
        }
    }

    companion object {
    
    
        private val TAG = HorizontalPopupLayout.javaClass.name
    }
}

其使用也就是需要将其包起来,且确保NavigationView在最后一个,其前一个元素也就是主布局文件内容。

<com.weizu.contentview.HorizontalPopupLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/horizontalPopupLayout"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        >
       ...
    </LinearLayout>

    <!-- 弹出菜单内容 -->
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"  
        android:fitsSystemWindows="true"
        app:itemTextColor="@color/text_color"
        app:headerLayout="@layout/nav_header"
        app:menu="@menu/nav_item" />

</com.weizu.contentview.HorizontalPopupLayout>

项目地址:https://github.com/baiyazi/NovelApp/tree/main/App

猜你喜欢

转载自blog.csdn.net/qq_26460841/article/details/124729830