ps: 文章较长适合电脑阅读
首先感谢 maimingliang
的开源项目,让我学到了很多。源码作者的github
学习Android一定会遇到产品上需要通过自定义View才能实现的控件,或者说为了提高编码效率通过自定View写一个公用的控件方便以后使用。自定义View也是学习Android必须要掌握的知识点之一。本篇文章将分析我在github上看到的一个开源的控件旨在总结一下自己学习自定义View的收获以及通过讲解让自己更加深刻的理解整个实现的过程。
这里是 BaseItemLayout
项目的github地址
这里顺便推荐一款chrome的插件方便在线浏览github叫
Octotree
在chrome应用商店里面可以搜索到
使用该插件你就能够通过树形文件结构快速的跳转到你想查看的文件了。
简单的介绍一下 BaseItemLayout
项目,就是自定义项目中常用到的列表布局,大家自行脑补微信 我的页面
的列表项。通过自定义该布局方便以后快速的添加此布局提高开发效率。
下面我们就来分析如何实现这个自定义View
先看一下整体的结构:
在 BaseItemLayout
中管理着许多 ItemView
,所以这里我们其实是需要自定义两个View。一个是 BaseItemLayout
另外一个就是列表项 ItemView
。
自定义 ItemView
如上图, ItemView
中有三个控件–行图标、行标题、箭头。
Step 1 创建ItemView
这里我们创建自定义View将继承 RelativeLayout
。
总体结构就是两大部分:
1. 初始化 ItemView
的三个控件。
2. 为这些控件添加一些控制样式的方法如:位置、大小、文本颜色……
代码如下:
public class ItemView extends RelativeLayout {
private Context context;
private ImageView icon;
private TextView title;
private ImageView arrow;
private LayoutParams iconLp;
private LayoutParams titleLp;
private LayoutParams arrowLp;
public ItemView(Context context) {
this(context, null);
}
public ItemView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ItemView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
public void initView(Context context) {
this.context = context;
icon = new ImageView(context);
title = new TextView(context);
arrow = new ImageView(context);
iconLp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
iconLp.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
addView(icon, iconLp);
titleLp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
titleLp.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
titleLp.addRule(RelativeLayout.RIGHT_OF, R.id.iv_icon);
addView(title, titleLp);
arrowLp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
arrowLp.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
arrowLp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
addView(arrow, arrowLp);
}
//---------为控件添加一些控制其属性的方法----------
//设置控件的相关属性(icon title arrow)
//setIconStyle() setTitleStyle() setArrowStyle()
//setLayoutParams() 设置ItemView列表项的高度
}
上面代码中分别定义好了 icon
title
arrow
这三个控件在 ItemView
中的位置。
在代码的最后添加了一些控制这三个控件的一些方法(此处省略具体代码)。到目前为止,我们就在 ItemView
上面画出来了这三个控件以及规定好了他们大致的位置。
在自定义View中,需要重写
ItemView
的构造方法,注意这三个构造方法中的前两个。第一个构造方法是调用的第二个构造方法,第二个则是调用的第三个构造方法。
Step 2 为ItemView新增控制样式的方法
写好了 ItemView
的控件,我们再来添加一些控制其控件样式的方法。
/**
* 设置图标样式
*
* @param iconMarginLeft 图标距离itemView左边的距离
* @param resId 图标资源
* @param height 图标的高度
* @param width 图标的宽度
*/
public void setIconStyle(int iconMarginLeft, int resId, int height, int width) {
// set icon margin left itemLayout
iconLp.leftMargin = DensityUtil.dip2px(context, iconMarginLeft);
// set pic for icon
icon.setImageResource(resId);
// set icon dimen
ViewGroup.LayoutParams layoutParams = icon.getLayoutParams();
layoutParams.height = height;
layoutParams.width = width;
icon.setLayoutParams(layoutParams);
}
/**
* 设置标题样式
*
* @param titleMarginLeft 标题距离图标左边的距离
* @param text 标题的内容
* @param textColor 标题的颜色
* @param textSize 标题文字的大小
*/
public void setTitleStyle(int titleMarginLeft, String text, int textColor, int textSize) {
// set title margin left
titleLp.leftMargin = DensityUtil.dip2px(context, titleMarginLeft);
// set title size and content
title.setText(text);
title.setTextColor(textColor);
title.setTextSize(DensityUtil.dip2px(context, textSize));
}
/**
* 设置箭头的样式
*
* @param isShow 是否显示箭头
* @param arrowMarginRight 箭头距离ItemView右边的距离
* @param resId 箭头图片资源
* @param height 箭头的高度
* @param width 箭头的宽度
*/
public void setArrowStyle(boolean isShow, int arrowMarginRight, int resId, int height, int width) {
// if show arrow
if (isShow) {
arrow.setVisibility(VISIBLE);
} else {
arrow.setVisibility(GONE);
}
arrowLp.rightMargin = DensityUtil.dip2px(context, arrowMarginRight);
// set image resource and dimen
arrow.setImageResource(resId);
ViewGroup.LayoutParams layoutParams = arrow.getLayoutParams();
layoutParams.height = height;
layoutParams.width = width;
arrow.setLayoutParams(layoutParams);
}
/**
* set ItemView height
* @param itemHeight
*/
public void setItemLayoutParams(int itemHeight) {
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
layoutParams.height = DensityUtil.dip2px(context, itemHeight);
setLayoutParams(layoutParams);
}
至此,我们已经创建好的了 ItemView
这个自定义控件。
Step 3 为ItemView添加自定义属性
为了能够在 XML
中通过属性来设置 ItemView
控件的相关属性,就像我们平常用系统中的属性如 android:layout_width="match_parent"
一样我们需要创建一个 attrs
文件来定义需要设置的属性。
创建 attrs
属性文件
<resources>
<declare-styleable name="ItemAttrs">
<attr name="item_height_jngoogle" format="integer"/>
<attr name="text_size_jngoogle" format="integer"/>
<attr name="text_color_jngoogle" format="color"/>
<attr name="icon_marginLeft_jngoogle" format="integer"/>
<attr name="title_marginLeft_jngoogle" format="integer"/>
<attr name="arrow_marginRight_jngoogle" format="integer"/>
<attr name="lineColor_jngoogle" format="color"/>
<attr name="isShow_jngoogle" format="boolean"/>
</declare-styleable>
这里设置了:
1. ItemView
的行高
2. 标题字体的大小以及颜色
3. 图标、标题、箭头的位置
4. ItemView
之间分割线的颜色
5. 是否显示箭头
OK,到这里我们已经把 ItemView
全部创建好了,现在就可以在你想使用的布局中使用我们自定义的View。
但是在实际的开发使用中,我们一般用到这种列表项的布局不会是只画一个,一般来说是多个所以为了更加方便的管理和更加快速的添加。我们又创建一个自定义View BaseItemLayout
来管理多个 ItemView
。
自定义BaseItemLayout
首先我们整理一下思路,我们要在 BaseItemLayout
中做什么事情。
1. 提供设置 ItemView
样式参数的set方法。
2. 绘制并把 ItemView
添加到 BaseItemLayout
中。
Step 1 初始化BaseItemLayout
同样的步骤我们首先要对 BaseItemLayout
初始化
public class BaseItemLayout extends LinearLayout {
private Context context;
private List<Integer> iconList = new ArrayList<>();
private List<String> titleList = new ArrayList<>();
//init icon title arrow attribute
private int iconHeight = 24;
private int iconWidth = 24;
private int iconMarginLeft = 10;
private int textSize = 14; // 14dp
private int textColor = 0xFF666666;
private int titleMarginLeft = 10; // 10dp
private int arrowResId = 0;
private int arrowHeight = 24;
private int arrowWidth = 16;
private int arrowMarginRight = 10;
private boolean isShow = false; // set arrow is show
// divide line color
private int lineColor = 0xff303F9F;
// item height
private int itemHeight = 40;
// distance between items
private SparseArray<Integer> itemsMarginArray = new SparseArray<>();
private static int DEFAOUT_ITEMS_MARGIN = 10;
private static int ZERO_ITEMS_MARGIN = 0;
public BaseItemLayout(Context context) {
this(context, null);
}
public BaseItemLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BaseItemLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ItemAttrs);
iconMarginLeft = a.getInteger(R.styleable.ItemAttrs_icon_marginLeft_jngoogle, iconMarginLeft);
textSize = a.getInteger(R.styleable.ItemAttrs_text_size_jngoogle, textSize);
textColor = a.getInteger(R.styleable.ItemAttrs_text_color_jngoogle, textColor);
titleMarginLeft = a.getInteger(R.styleable.ItemAttrs_title_marginLeft_jngoogle, titleMarginLeft);
arrowMarginRight = a.getInteger(R.styleable.ItemAttrs_arrow_marginRight_jngoogle, arrowMarginRight);
lineColor = a.getInteger(R.styleable.ItemAttrs_lineColor_jngoogle, lineColor);
itemHeight = a.getInteger(R.styleable.ItemAttrs_item_height_jngoogle, itemHeight);
isShow = a.getBoolean(R.styleable.ItemAttrs_isShow_jngoogle, isShow);
a.recycle();
init(context);
}
// create()方法 -- 绘制BaseItemLayout
// addItem()方法 -- 添加 itemView 到 BaseItemLayout 中
//一些参数值得set方法
//ex:setIconWidth() 设置图标的宽度
}
注意
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ItemAttrs);
得到当前 BaseItemLayout
的属性数组,之后我们在 XML
中设置的值就可以作为参数在方法中使用。
特别提醒别忘记 a.recycle();
Step 2 绘制BaseItemLayout,把ItemView添加到BaseItemLayout中
步骤:
1. 设置好 ItemView
的控件样式。
2. 依次把 ItemView
添加到 BaseItemLayout
中。
/**
* create baseItemLayout
*/
public void create() {
if (iconList.isEmpty()) {
throw new RuntimeException("iconList is null");
}
if (titleList.isEmpty()) {
throw new RuntimeException("titleList is null");
}
if (iconList.size() != titleList.size()) {
throw new RuntimeException("params not match, icon's sum should be equal title's sum");
}
for (int i = 0; i < iconList.size(); i++) {
ItemView itemView = new ItemView(context);
itemView.setIconStyle(iconMarginLeft, iconList.get(i), iconHeight, iconWidth);
itemView.setTitleStyle(titleMarginLeft, titleList.get(i), textColor, textSize);
itemView.setArrowStyle(isShow, arrowMarginRight, arrowResId, arrowHeight, arrowWidth);
itemView.setItemLayoutParams(itemHeight);// set item height
addItem(itemView, i);
}
}
这里通过
setIconStyle()
setTitleStyle()
setArrowStyle()
setItemLayoutParams()
这四个方法设置好ItemView
中控件的样式。
然后通过addItem()
方法将ItemView
添加到BaseItemLayout
addItem()
方法:
/**
* 添加ItemView布局到baseItemLayout
*
* @param itemView
* @param pos the position of current itemView
*/
private void addItem(ItemView itemView, int pos) {
if (itemsMarginArray.get(pos) != null) {
if (itemsMarginArray.get(pos) > 0) {
addView(createLineView(itemsMarginArray.get(pos)));
}
} else {
addView(createLineView(DEFAOUT_ITEMS_MARGIN));
}
addView(itemView);
addView(createLineView(ZERO_ITEMS_MARGIN));// item的下面的分割线
}
这里按照 上分割线 -> itemView -> 下分割线 的顺序绘制。这样就把 ItemView
绘制到 BaseItemLayout
中。
剩下的就是提供图片、文字、箭头的方法,设置样式的方法,此处省略。
设置样式的方法
至此,已经完成了 BaseItemLayout
的自定义。最后我们来看一下如何使用自定义的View
使用方法
public class MainActivity extends AppCompatActivity {
private BaseItemLayout baseItemLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
baseItemLayout = (BaseItemLayout) findViewById(R.id.base_item_layout);
initData();
}
private void initData() {
List<Integer> iconList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
titleList.add("photo");
titleList.add("favorite");
titleList.add("wallet");
titleList.add("cardCase");
titleList.add("settings");
iconList.add(R.mipmap.xc);
iconList.add(R.mipmap.sc);
iconList.add(R.mipmap.qb);
iconList.add(R.mipmap.kb);
iconList.add(R.mipmap.sz);
baseItemLayout.setTitleList(titleList)
.setIconList(iconList)
.setArrowIsShow(true)
.setArrowResId(R.mipmap.img_find_arrow)
.setItemsMargin(0,0)
.create();
}
}
整个自定义View的流程就是这么多了,其中一些具体的方法需要读者自行去阅读。总体来说自定义View的步骤:
1. 写好自定义View的布局(也可以省略,在java中绘制出来)。
2. 创建自定义View, 设置好相对应的方法。