标题栏控件是在App开发中运用最为广泛的一类控件,运用频率在榜首梯队。Google跟苹果都很注重这样的一个控件,所以Android就有了ActionBar,苹果就有了UINavigationController。那么已然Android官方界说了actionbar,为什么还要自己界说标题栏呢?我觉得原因有许多,其中最主要的原因便是运用不方便,坑多。当然actionbar也有它的优点,那便是功用强大。我写这个控件的目的并不能彻底覆盖actionbar的一切功用,只是在这种相似这样界面的标题栏,写代码会很高效。
效果图
思路分析
咱们分析一下完成这样一个通用的标题栏控件,需求留意什么?首要,这个标题栏分为3部分,左面是一个回来键按钮,中间是一个标题文本,右边是几个菜单按钮。咱们能够经过自界说特点,配置标题、是否显现标题、标题的字体巨细,以及标题文本是否加粗。回来键应该是要能够修改回来的图标、回来键的巨细,以及间隔左面边际的间隔。最最重要的来了,那便是回来键会经常被运用,所以对点击事情的交互体验有一定要求。要不然,点了半响,点不到,也回来不了界面,这样会让用户很气愤,想砸手机。关于这个问题,咱们的优化方案是,在按钮外面人为加个容器,强制增大点击事情的响应规模。这样就给用户手机很活络的幻觉。最终是右边的功用菜单按钮,它们是水平排列的,从右向左下标依次是0、1、2。
如何运用
咱们要有逆向思想,先思考别人调咱们的API大概是怎样的代码,然后再开始封装。
// 增加以下代码到项目根目录下的build.gradle
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
// 增加以下代码到app模块的build.gradle
dependencies {
implementation 'com.github.dora4:dview-titlebar:1.7'
}
val imageView = AppCompatImageView(this)
val dp24 = DensityUtils.dp2px(this, 24f)
imageView.layoutParams = RelativeLayout.LayoutParams(dp24, dp24)
imageView.setImageResource(R.drawable.ic_save)
val imageView2 = AppCompatImageView(this)
imageView2.layoutParams = RelativeLayout.LayoutParams(dp24, dp24)
imageView2.setImageResource(R.drawable.ic_confirm)
mBinding.titleBar
.addMenuButton(imageView)
.addMenuButton(imageView2)
.setOnIconClickListener(object : DoraTitleBar.OnIconClickListener {
override fun onIconBackClick(icon: AppCompatImageView) {
LogUtils.i("回来")
}
override fun onIconMenuClick(position: Int, icon: AppCompatImageView) {
LogUtils.i("点击了第${position}个菜单")
}
})
代码完成
<resources>
<declare-styleable name="DoraTitleBar">
<attr name="dview_isShowBackIcon" format="boolean"/>
<attr name="dview_backIcon" format="reference"/>
<attr name="dview_backIconSize" format="dimension|reference"/>
<!-- 用于给回来按钮增加点击区域 -->
<attr name="dview_backIconBoxPadding" format="dimension|reference"/>
<attr name="dview_backIconMarginStart" format="dimension|reference"/>
<attr name="dview_isClickBackIconClose" format="boolean"/>
<attr name="dview_isShowTitle" format="boolean"/>
<attr name="dview_title" format="string|reference"/>
<attr name="dview_titleTextColor" format="color|reference"/>
<attr name="dview_titleTextSize" format="dimension|reference"/>
<attr name="dview_isTitleTextBold" format="boolean"/>
<!-- 用于给菜单按钮增加点击区域 -->
<attr name="dview_menuIconBoxPadding" format="dimension|reference"/>
<attr name="dview_menuIconMarginEnd" format="dimension|reference"/>
</declare-styleable>
</resources>
以上便是这个自界说控件的一切自界说特点,下面我来别离解说。
dview_isShowBackIcon:是否显现左面的回来按钮,默许true
dview_backIcon:自界说回来按钮的图标
dview_backIconSize:回来按钮的尺度
dview_backIconBoxPadding:给回来按钮增加的点击区域的巨细
dview_backIconMarginStart:回来按钮距屏幕左面缘的间隔
dview_isClickBackIconClose:点击回来按钮是否关闭当前activity
dview_isShowTitle:是否显现标题
dview_title:标题文本
dview_titleTextColor:标题字体颜色
dview_titleTextSize:标题字体巨细
dview_isTitleTextBold:标题文本是否加粗
dview_menuIconBoxPadding:给菜单按钮增加的点击区域的巨细
dview_menuIconMarginEnd:第0个菜单按钮距屏幕右边际的间隔
老生常谈了,首要肯定是解析自界说特点。
val a = context.obtainStyledAttributes(attrs, R.styleable.DoraTitleBar)
isShowBackIcon = a.getBoolean(R.styleable.DoraTitleBar_dview_isShowBackIcon, isShowBackIcon)
backIcon = a.getDrawable(R.styleable.DoraTitleBar_dview_backIcon) ?: backIcon
backIconSize = a.getDimensionPixelSize(R.styleable.DoraTitleBar_dview_backIconSize, backIconSize)
backIconMarginStart = a.getDimensionPixelSize(R.styleable.DoraTitleBar_dview_backIconMarginStart, backIconMarginStart)
menuIconMarginEnd = a.getDimensionPixelSize(R.styleable.DoraTitleBar_dview_menuIconMarginEnd, menuIconMarginEnd)
backIconBoxPadding = a.getDimensionPixelOffset(R.styleable.DoraTitleBar_dview_backIconBoxPadding, backIconBoxPadding)
menuIconBoxPadding = a.getDimensionPixelOffset(R.styleable.DoraTitleBar_dview_menuIconBoxPadding, menuIconBoxPadding)
isClickBackIconClose = a.getBoolean(R.styleable.DoraTitleBar_dview_isClickBackIconClose, isClickBackIconClose)
isShowTitle = a.getBoolean(R.styleable.DoraTitleBar_dview_isShowTitle, isShowTitle)
title = a.getString(R.styleable.DoraTitleBar_dview_title) ?: title
titleTextColor = a.getColor(R.styleable.DoraTitleBar_dview_titleTextColor, titleTextColor)
titleTextSize = a.getDimensionPixelSize(R.styleable.DoraTitleBar_dview_titleTextSize, titleTextSize)
isTitleTextBold = a.getBoolean(R.styleable.DoraTitleBar_dview_isTitleTextBold, isTitleTextBold)
a.recycle()
忘了个事情,咱们这个标题栏控件继承自何种控件?没错,我想你应该想到了,那便是RelativeLayout。咱们会把这些控件都增加到相对布局中,这需求运用kotlin的代码进行动态布局。因为左面的回来按钮和标题栏文字是事先摆放好的,所以咱们直接先制作上去。右侧的功用菜单按钮列表,咱们采用代码动态增加的方法,因为咱们并不知道总共有多少个,也可能没有。但据我观测,手机最多也就能放3~4个,平板会多点。
咱们解析完自界说特点后,会对一些初始化的控件进行动态布局。
private fun initView(context: Context) {
val titleLp = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
titleLp.addRule(CENTER_IN_PARENT)
val backIconBoxLp = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
backIconBoxLp.marginStart = backIconMarginStart
backIconBoxLp.addRule(ALIGN_PARENT_START)
backIconBoxLp.addRule(CENTER_VERTICAL)
val menuIconContainerLp = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
menuIconContainerLp.marginEnd = menuIconMarginEnd
menuIconContainerLp.addRule(ALIGN_PARENT_END)
menuIconContainerLp.addRule(CENTER_VERTICAL)
backIconBox = wrapButton(true, backIconView)
if (isShowBackIcon) addView(backIconBox, backIconBoxLp)
if (isShowTitle) addView(titleView, titleLp)
menuIconContainer.gravity = Gravity.CENTER_VERTICAL
menuIconContainer.orientation = LinearLayoutCompat.HORIZONTAL
addView(menuIconContainer, menuIconContainerLp)
backIconView.background = backIcon
titleView.text = title
titleView.textSize = px2sp(context, titleTextSize.toFloat())
titleView.setTextColor(titleTextColor)
}
private fun initListener(context: Context) {
backIconBox.setOnClickListener {
onIconClickListener?.onIconBackClick(backIconView)
if (isClickBackIconClose) {
(context as Activity).finish()
}
}
}
如何将控件制作到ViewGroup上呢?或者说将view制作到ViewGroup的流程是怎样的?这是一个要点。首要要对这个布局容器的一切自绘子控件进行丈量,咱们运用measureChild()方法。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, applyWrapContentSize(heightMeasureSpec, dp2px(context, 48f)))
if (isShowBackIcon) measureChild(backIconBox, widthMeasureSpec, heightMeasureSpec)
if (isShowTitle) measureChild(titleView, widthMeasureSpec, heightMeasureSpec)
}
咱们要考虑到有些开发者喜欢运用wrap_content作为标题栏的高度,没问题,咱们给它个默许的高度,这样不至于显现不出控件。
/**
* 设置wrap_content的情况下,给定默许宽高。
*
* @param expected 期望的值
*/
private fun applyWrapContentSize(measureSpec: Int, expected: Int): Int {
var measureSpec = measureSpec
val mode: Int = MeasureSpec.getMode(measureSpec)
if (mode == MeasureSpec.UNSPECIFIED
|| mode == MeasureSpec.AT_MOST
) {
measureSpec = MeasureSpec.makeMeasureSpec(expected, View.MeasureSpec.EXACTLY)
}
return measureSpec
}
很明显,运用者界说wrap_content为高度的时分,咱们得到的丈量标准MeasureSpec的值为MeasureSpec.AT_MOST。未指定高度,咱们也给它个默许值。
然后接下来一步,便是将丈量好的子控件,制作到ViewGroup上。咱们一般会重写onDraw()方法,咱们运用dispatchDraw()方法其实更契合单一责任规划准则。dispatchDraw()方法会在制作完自身xml中界说的子控件后,再添枝加叶,哦不,再增加一些制作。这些便是咱们自己用代码制作的view了。
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
if (isShowBackIcon) drawChild(canvas, backIconBox, drawingTime)
if (isShowTitle) drawChild(canvas, titleView, drawingTime)
}
这便是完好的在ViewGroup上制作View的流程。
最终咱们再看一下右侧的菜单按钮的完成。
fun addMenuButton(menuIconView: AppCompatImageView) : DoraTitleBar {
val menuBox = wrapButton(false, menuIconView)
menuBox.setOnClickListener {
if (menuBox.childCount > 0) {
val imageView = menuBox.getChildAt(0) as AppCompatImageView
menuBoxList.forEachIndexed { index, frameLayout ->
if (menuBox == frameLayout) {
onIconClickListener?.onIconMenuClick(index, imageView)
}
}
}
}
// 增加到最前面去,这是因为索引从右边向左面递增
menuIconContainer.addView(menuBox, 0)
menuBoxList.add(menuBox)
return this
}
/**
* 增加按钮的点击区域规模。
*/
private fun wrapButton(isBackButton: Boolean, iconView: AppCompatImageView) : FrameLayout {
val box = FrameLayout(context)
if (isBackButton) {
box.setPadding(backIconBoxPadding, backIconBoxPadding, backIconBoxPadding, backIconBoxPadding)
val lp = FrameLayout.LayoutParams(backIconSize, backIconSize)
box.addView(iconView, lp)
} else {
box.setPadding(menuIconBoxPadding, menuIconBoxPadding, menuIconBoxPadding, menuIconBoxPadding)
box.addView(iconView)
}
return box
}
咱们在增加进来的时分,直接设置点击事情,因为咱们最多也就几个菜单按钮,所以运用循环去比对控件得到点击的下标,性能开销也不大。
最终,咱们再增加一个点击事情,就功德圆满了!
interface OnIconClickListener {
/**
* 左面的回来键按钮被点击。
*/
fun onIconBackClick(icon: AppCompatImageView)
/**
* 右侧的菜单按钮被点击。
*/
fun onIconMenuClick(position: Int, icon: AppCompatImageView)
}
fun setOnIconClickListener(listener: OnIconClickListener) {
onIconClickListener = listener
}
咱们这里回调AppCompatImageView,而不回调增加点击规模的容器的原因也很简单,那便是最小常识规划准则,你用不到的就不提供给你。我想充其量最多也就换一下菜单按钮的颜色或图标的状况罢了。
开源库地址
github.com/dora4/dview…
示例代码地址
github.com/dora4/dora_…