菜单式控件——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>
布局文件很简单:
- 左侧有一个MarqueeTextView控件,用于显示菜单文本,同时该文本支持设定DrawableLeft,也就是icon图标;单行显示,若文本过程则以跑马灯形式显示。
- 右侧则是一个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是否加入计数体系的判断逻辑:
- 如果 LineMenuView_for_calculation 属性 取值为 bypassed(xml不设置的话也是bypassed状态);则表示采取默认操作;
- 默认操作:如果 LineMenuView可见性为View.VISIBLE,则加入计数体系。如果为其他不可见状态(gone,invisible)则不加入。
- 如果LineMenuView_for_calculation 属性 取值为 on,则表示开启计数,此时无论控件是否可见都会加入计数体系。
- 如果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自定义属性值:
- 需要使用的插件类型;比如 text,transition等,这个通过app:LineMenuView_plugin属性指定。
- 插件初始化;比如menu和brief文字,大小,颜色;或者transition默认是选中还是非选中等等。这个通过attr.xml文件中列出的属性值就可以对照设置。
- 考虑是否需要纳入计数系统,如果纳入的话,不光自身的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