通用式菜单式控件——LineMenuView

菜单式控件——LineMenuView

就目前来说,Android业界各种框架层出不穷,开发时使用通用的框架搭建模型,然后填充数据与业务逻辑即可。
不过对于一些比较“小型”的界面,一般都需要自己封装类来进行操作,比如一些菜单项,按钮样式等等。
这里基于平常使用菜单类型封装成了Menu菜单,样式比较简单,不过可以明显加快开发进度。
下面具体介绍框架的结构

一、显示效果

针对不同插件类型的效果图如下:

1、静态效果

静态显示

2、动图效果

针对一些可以使用的控件,添加响应效果,显示效果如下:

动图效果

点击按钮则会触发默认的点击事件,来切换状态或进行其他业务处理。

二、插件说明

LineMenuView控件插件类型如下:

  • 无插件形式,只有单独的左侧menu文本,可显示简单菜单信息。
  • text插件形式,右侧会多出一个TextView,被称作 “brief”信息,可以设置文本以及 Drawable***
  • transition形式 切换两种图片,根据透明度淡入或淡出
  • select形式 切换选中/未选中两种状态
  • radio模式 radio形式的选中切换
  • switch_形式,之所以会在最后添加下划线,是因为switch属于关键字,无法直接使用;该类型菜单可以切换开关按钮。

基本插件类型只有这五种(不包括无插件形式),一般 text插件 使用最多,下面就源码及布局来说明插件的实现方式:

1、LineMenuView布局方式

<?xml version="1.0" encoding="utf-8"?>
<merge
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:minHeight="@dimen/prefer_view_height_48dp"
    android:orientation="horizontal">
    <!--menu前端为标题-->
    <com.knowledge.mnlin.linemenuview.MarqueeTextView
        android:id="@+id/tv_menu"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:drawablePadding="@dimen/view_padding_margin_12dp"
        android:ellipsize="marquee"
        android:gravity="start|center_vertical"
        android:marqueeRepeatLimit="marquee_forever"
        android:singleLine="true"
        tools:text="对应的菜单信息"/>

    <!--menu后面为可选择部分-->
    <FrameLayout
        android:id="@+id/fl_plugin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingEnd="0dp"
        android:paddingStart="@dimen/view_padding_margin_8dp">
        <!--使用单个TextView,文本右对齐-->
        <TextView
            android:id="@+id/tv_brief_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:drawablePadding="@dimen/view_padding_margin_12dp"
            android:ellipsize="end"
            android:gravity="center_vertical|end"
            android:maxLines="1"
            android:visibility="gone"
            tools:text="右侧文本内容"/>

        <!--使用开关按钮,可切换on/off状态-->
        <android.support.v7.widget.SwitchCompat
            android:id="@+id/sc_switch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center_vertical|end"
            android:visibility="gone"/>

        <!--使用携带了一个Radio的视图-->
        <RadioButton
            android:id="@+id/rb_check"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:button="@drawable/selector_radio_checked"
            android:checked="true"
            android:gravity="center_vertical|end"
            android:minHeight="@dimen/view_height_16dp"
            android:minWidth="@dimen/view_height_16dp"
            android:visibility="gone"/>

        <!--使用单个图片,对于需要动态变化的图像,使用drawable属性会不方便,/选中/未选中-->
        <ImageView
            android:id="@+id/iv_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center_vertical|end"
            android:src="@drawable/icon_choose_address"
            android:visibility="gone"/>

        <!--渐变的图片切换-->
        <ImageView
            android:id="@+id/icon_open_close"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center_vertical|end"
            android:src="@drawable/transition_close_open"
            android:visibility="gone"/>
    </FrameLayout>
</merge>

布局文件很简单:

  1. 左侧有一个MarqueeTextView控件,用于显示菜单文本,同时该文本支持设定DrawableLeft,也就是icon图标;单行显示,若文本过程则以跑马灯形式显示。
  2. 右侧则是一个FramLayout容器,所有类型插件对应的View都放在里面,通过visible属性来控制显示或隐藏。

通过控件中内置的方法setPlugin就可以看到具体的逻辑:

/**
 * @param plugin 0 表示不显示任何插件
 *               1 表示显示textView
 *               2 表示显示switch
 *               3 表示显示radio
 *               4 表示select
 *               5 表示transition
 */
public LineMenuView setPlugin(int plugin) {
    mTvBriefInfo.setVisibility(plugin == 1 ? View.VISIBLE : View.GONE);
    mScSwitch.setVisibility(plugin == 2 ? View.VISIBLE : View.GONE);
    mRbCheck.setVisibility(plugin == 3 ? View.VISIBLE : View.GONE);
    mIvImage.setVisibility(plugin == 4 ? View.VISIBLE : View.GONE);
    mIconOpenClose.setVisibility(plugin == 5 ? View.VISIBLE : View.GONE);
    return this;
}

如此一来,就可以根据插件码(也可根据后面说到的style属性来区分)控制插件显示的类型;

2、插件实际效果与名称对应关系

这里给出text插件transition插件显示效果及对应位置说明:

transition插件:

这里写图片描述

text插件:

这里写图片描述

3、attr属性(自定义属性及用途)

LineMenuView定了多种属性,在使用插件时,可以在xml中使用属性来定义需要显示的效果。所有的属性值如下:

<!--单行菜单对应的参数:switch状态、menu文本、icon图标等-->
    <declare-styleable name="LineMenuView">
        <attr name="LineMenuView_plugin" format="enum">
            <enum name="none" value="0"/>
            <enum name="text" value="1"/>
            <enum name="switch_" value="2"/>
            <enum name="radio" value="3"/>
            <enum name="select" value="4"/>
            <enum name="transition" value="5"/>
        </attr>
        <attr name="LineMenuView_switch" format="enum">
            <enum name="off" value="0"/>
            <enum name="on" value="1"/>
        </attr>
        <!--选中/未选中-->
        <attr name="LineMenuView_radio" format="enum">
            <enum name="off" value="0"/>
            <enum name="on" value="1"/>
        </attr>
        <!--开/关-->
        <attr name="LineMenuView_transition" format="enum">
            <enum name="off" value="0"/>
            <enum name="on" value="1"/>
        </attr>
        <!--用于计算,default表示默认:只有在visible时才会纳入计算;on表示纳入计算,即便是不可见状态;off表示不纳入计算,即使是可见状态-->
        <attr name="LineMenuView_for_calculation" format="enum">
            <enum name="bypassed" value="0"/>
            <enum name="on" value="1"/>
            <enum name="off" value="2"/>
        </attr>
        <attr name="LineMenuView_badge" format="reference"/>
        <attr name="LineMenuView_navigation" format="reference"/>
        <attr name="LineMenuView_icon" format="reference"/>
        <attr name="LineMenuView_brief" format="string"/>
        <attr name="LineMenuView_menu" format="string"/>
        <attr name="LineMenuView_brief_text_color" format="color"/>
        <attr name="LineMenuView_menu_text_color" format="color"/>
        <attr name="LineMenuView_brief_text_size" format="dimension"/>
        <attr name="LineMenuView_menu_text_size" format="dimension"/>
    </declare-styleable>

这里定义了menu以及brief文字大小颜色,还有各个插件的初始状态;一些名字对应上面的效果图,很容易分辨出功能来,因此不多做说明。

唯一需要解释的便是 LineMenuView_for_calculation 属性,该属性是服务于点击事件的。

其实在控件初始化时,其实已经默认绑定了LineMenuView的点击事件监听器:

//如果当前view所在的context对象声明了该接口,那么就直接进行绑定
if (getContext() instanceof LineMenuListener && setListenerIsSelf()) {
    setOnClickListener((LineMenuListener) getContext());
}

在段代码是在 initData方法 中执行的,而initData方法则是在构造函数中调用的,因此只要LineMenuView所在的上下文环境Context实现了LineMenuListener接口,就无需使用者主动调用监听器设置方法了。

再看上面的监听器设置方法,这里除了判断Context是否实现了LineMenuListener接口外,还判断了setListenerIsSelf方法的逻辑。

因此如果使用者不想默认添加监听事件,只需要自定义一个class类继承LineMenuView,、将setListenerIsSelf方法重写返回false即可,就像这样:

/**
 * 是否设置onClickLisener为自身this
 *
 * @return true表示设置
 */
protected boolean setListenerIsSelf() {
    return false;
}

接着我们继续谈论 LineMenuView_for_calculation 属性 的功能,该属性值其实是为了控制LineMenuView控件 是否设置表示自身位置的tag属性,以及自身是否纳入计数体系

这个说明起来可能比较拗口,现在先看一下 什么是计数体系

① LineMenuView的计数处理

前面已经说过,使用该控件时,如果Context已经实现了 LineMenuListener 接口,那么就无需再主动设置监听事件了。
那么使用者怎么知道被点击的LineMenuView是哪一个呢(如果一个布局中出现了多个LineMenuView控件的话)?

可以先查看一下LineMenuListener 接口的回调方法:

/**
  * 控件监听
  */
 public interface LineMenuListener {
     /**
      * 点击左侧文本
      *
      * @param v 被点击到的v;此时应该是左侧的TextView
      * @return 是否消费该点击事件, 如果返回true, 则performSelf将不会被调用
      */
     boolean performClickLeft(TextView v);

     /**
      * 注:该放置主要针对 text 插件设计,但即便是其他插件模式,也可以通过 v.getTag()方法获取到位置信息
      * 因为就menu菜单来说,也只有文本形式才会取考虑点击左侧和右侧时有不同的处理逻辑
      * 
      * @param v 被点击到的v;此时应该是右侧的TextView
      * @return 是否消费该点击事件, 如果返回true, 则performSelf将不会被调用
      */
     boolean performClickRight(TextView v);

     /**
      * @param v 被点击到的v;此时应该是该view自身:LineMenuView
      */
     void performSelf(LineMenuView v);
 }

当然,可以通过判断是否是同一个View,来处理不同逻辑,以 performSelf方法 来说,可以通过判断 v 与某个布局中的控件是否是同一个来执行不同的代码。

但如果xml中有多个这样的布局,则处理起来会相当麻烦,因此在这三个回调方法执行时,就已经设置好了一个TAG值,通过该TAG值可以获取到某个控件在布局中所处的位置(非LineMenuView的控件不计算在内)。

就像这样:

/**
 * @param v 被点击到的v;此时应该是该view自身:LineMenuView
 */
@Override
public void performSelf(LineMenuView v) {
    int position = ((int) v.getTag(LineMenuView.TAG_POSITION));
    switch (position) {
        //因为第一个LineMenuView设置了LineMenuView_for_calculation为off,表示不计数,因此会是-1,且不会影响它后面LineMenuView的序号
        //但是,因为开始时候给mLmvFirst重新设定了onClickListener方法,因此点击它时根本不会进入该方法(performSelf内)

        case 0://文本形式
            showToast("文本形式:位置 0");
            break;
        case 1://可改变字体颜色大小的文本形式
            showToast("可改变字体颜色大小的文本形式: 位置 1");
            break;
        case 2://带箭头(navigation)、badge图标的形式
            showToast("带箭头、badge图标的形式 : 位置 2");
            break;
        case 3://带箭头、icon、badge,且menu信息滚动的形式
            showToast("带箭头、icon、badge,且menu信息滚动的形式: 位置 3");
            break;
        case 4://transition模式
            showToast("transition模式: 位置 4");
            v.setTransition(!v.getTransition());
            break;
        case 5://select模式
            showToast("select模式: 位置 5");
            v.setRightSelect(!v.getRightSelect());
            break;
        case 6://radio模式
            showToast("radio模式: 位置 6");
            v.setRadio(!v.getRadio());
            break;
        case 7://switch_模式
            showToast("switch_模式: 位置 7");
            v.setSwitch(!v.getSwitch());
            break;
    }
}

这段代码即是上面效果图对应的代码,使用TAG的话就可以不对xml中控件设置id属性,直接通过position判断需要进行的逻辑。

还有一个问题就是,可能某个LineMenuView在某个版本中默认不可见,如果此时不进行额外处理的话,TAG取值会出现偏差,因此就引入了LineMenuView_for_calculation 属性控制TAG取值效果。

这里需要说明的是,即便某个LineMenuView不引入计数体系,也不会影响 LineMenuListener 监听器的设置(如果Context实现了该接口的话),只是这时候在 performSelf中获取TAG值时为 -1 罢了;这样也相当于给不引入计数的控件留下了可操作的空间。

现在给出LineMenuView是否加入计数体系的判断逻辑:

  1. 如果 LineMenuView_for_calculation 属性 取值为 bypassed(xml不设置的话也是bypassed状态);则表示采取默认操作;
  2. 默认操作:如果 LineMenuView可见性为View.VISIBLE,则加入计数体系。如果为其他不可见状态(gone,invisible)则不加入
  3. 如果LineMenuView_for_calculation 属性 取值为 on,则表示开启计数,此时无论控件是否可见都加入计数体系。
  4. 如果LineMenuView_for_calculation 属性 取值为 off,则表示不开启计数,此时无论控件是否可见都不会加入计数体系。

就像上面的模版那样,第一个LineMenuView设置LineMenuView_for_calculation属性为off,因此虽然控件处于可见状态,但还是不会纳入计数体系;
不过就像之前所说,即便不会纳入计数体系也不会影响监听事件的相应情况,如果第一个LineMenuView没有调用setOnClickListener方法修改监听器,那么点击该控件在回调方法中还是可以获取到TAG对应的值的,只不过恒为 -1 而已。

4、监听器LineMenuListener

上面已经基本介绍了LineMenuListener回调如果使用。即通过TAG取值判断对应的控件(不仅仅是performSelf方法,其他两个perform***方法也可以通过TAG来获取位置值)。

还有一些没提到的就是这监听器其实是三个回调,分别为:

  • 点击左侧 menu菜单信息,会调用performClickLeft方法,如果该方法返回true,performSelf方法将不会执行。
  • 点击右侧 各种插件 ,会调用performClickRight方法,如果该方法返回true,performSelf方法将不会执行。
  • 当以上两个方法返回false时,该方法才有得以执行的机会。该方法没有返回值,通常用于处理点击左侧和右侧都具有相同逻辑的事情。

三、使用步骤

接下来说明如何使用该控件,事实上这很简单:

1、在xml中声明控件

<com.knowledge.mnlin.linemenuview.LineMenuView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/view_padding_margin_12dp"
    app:LineMenuView_badge="@mipmap/mobile_black"
    app:LineMenuView_navigation="@drawable/icon_arrow_right"
    android:background="@color/white_background_5"
    android:paddingEnd="@dimen/view_padding_margin_16dp"
    android:paddingStart="@dimen/view_padding_margin_16dp"
    app:LineMenuView_brief="简要信息"
    app:LineMenuView_icon="@mipmap/mobile_blue"
    app:LineMenuView_menu="带icon的简要信息,且信息太长需要一直滚动滚动滚动滚动滚动滚动滚动滚动滚动滚动"
    app:LineMenuView_plugin="text"/>

<com.knowledge.mnlin.linemenuview.LineMenuView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/view_padding_margin_12dp"
    android:background="@color/white_background_5"
    android:paddingEnd="@dimen/view_padding_margin_16dp"
    android:paddingStart="@dimen/view_padding_margin_16dp"
    app:LineMenuView_menu="切换模式"
    app:LineMenuView_plugin="transition"
    app:LineMenuView_transition="on"/>

上面代码展示的是: “二:2、插件实际效果与名称对应关系” 中两种插件显示效果对应的代码;一般分三个步骤考虑设置LineMenuView自定义属性值:

  1. 需要使用的插件类型;比如 text,transition等,这个通过app:LineMenuView_plugin属性指定。
  2. 插件初始化;比如menu和brief文字,大小,颜色;或者transition默认是选中还是非选中等等。这个通过attr.xml文件中列出的属性值就可以对照设置。
  3. 考虑是否需要纳入计数系统,如果纳入的话,不光自身的TAG取值会有变化,连同其他兄弟节点(仅指LineMenuView类型)的TAG值也可能会发生变化。这个通过app:LineMenuView_for_calculation属性来设置。

此时,基本的显示效果已经完成。

2、对自己的 **Activity 声明实现LineMenuListener接口

接下来,就需要让Context实现接听接口了。
当然也可以不使用默认的方法,但那样就需要自己去声明LineMenuView**对应成员变量,为xml中LineMenuView添加 **id 属性,然后通过ButterKnife或者findViewById方法为成员变量赋值。
然后还对每一个LineMenuView控件调用setOnClickListener方法设置监听器。

这样实在是太麻烦了,使用框架的目的就是为了简单方便,如果需要做那么多操作,还不如直接手搓代码来的爽快。

public class TestActivityActivity extends BaseActivity<TestActivityPresenter> implements TestActivityContract.View, LineMenuView.LineMenuListener {
    //...
}

就像这样,然后再实现三个方法即可。

3、填充处理逻辑

就像上面所说,通过TAG标签来获取所处的位置,然后通过switch 处理逻辑:

int position = ((int) v.getTag(LineMenuView.TAG_POSITION));
switch (position) {
    //...
}

四、自定义显示效果

因为自身控件代码量很小,即便拷贝粘贴到自己的项目中也不会花费什么时间,因此这里只提供了修改全局xml文件的方法:

/**
 * 位置信息情况
 */
public static final int TAG_POSITION = R.id.LINE_MENU_VIEW_TAG_POSITION;

/**
 * 布局文件
 */
public static final int LAYOUT_SELF = R.layout.layout_line_menu;

看到这个应该都明白了吧,想要修改整个布局方法很简单,直接修改静态变量就可以了,一般来说一个项目中代码风格都是统一的,应该不会出现变来变去需要多套场景的情况。

不过需要注意的时,自定义xml文件时一定是要把源xml文件给拷贝下来,必须里面的有id值的控件不能变!!!,否则可能会 出现异常。

如果改动很大,推荐直接拷贝源码进行修改,代码量只有数百。

详细的仓库使用方式可以参考GITHUB;

源码地址:GITHUB

猜你喜欢

转载自blog.csdn.net/lovingning/article/details/79624457
今日推荐