之前一直有写过自定义View,自定义ViewGroup。但是每次再写都会重新去网上搜索。的确这是我们大部分人的习惯,但是有次和公司一个大牛聊天。他告诉我说,一次两次从网上搜索没问题,但是很多次遇到同样的问题还是只想着去搜索就是自己的问题了,你可能根本没有自己去思考过。于是结合做的项目的一些小功能总结一下Android自定义View和自定义ViewGroup。
首先要实现的效果是下图所示的中右侧的索引部分。我们要做的很简单,就是显示字母地区首字母的索引。
1.分析一下上图中的索引的实现,首先看需要实现的功能。
当手指在这个View上滑动的时候,外面的地区列表显示的地区名称的首字母需要和你手指触摸的字母相同。另外当你中间有个View会显示你当前触摸到的字母。所以首先这个view需要对外提供一个接口,告诉外面手指触摸到的字母是什么。中间那个字母的显示可以通过把字母传出去在外面显示,也可以把textview传递进来。这里是我通过把textview传递进来显示。提供的接口如下:
/**
* 对外接口,将indexview选中的字符传递出去
*/
interface OnSelectedListener {
void onSelected(String s);
}
/**
* 用于将外界的textview设置进来,控制其显示
* @param view
*/
public void setCenterTextView(TextView view) {
centerTextView = view;
}
2.然后分析下这个View本身该怎么显示出来。
这个View本身的显示很简单的,就是显示地区的首字符。如果你需要的就是26个字母的话,可以直接在View里面给一个字符数组写死。我这里是想显示地区的首字母,可能没有地区的首字母是x,于是我这个地方显示的字母是由外部传入的。传进来之后调用invalidate()方法实现界面刷新即可。另外提一句,如果你要是想在非ui线程中刷新就使用postInvalidate()方法,这个方法在使用handler帮你完成了线程切换工作。
/**
* 设置IndexView要显示的索引
* @param indexList
*/
public void setIndexList(List indexList) {
if (null == indexList) {
return;
}
mIndexList = indexList;
invalidate();
}
接着是字母的颜色,以及字母的大小和字母间的间隔,我们通过自定义属性来设置。一般我们是把这个放在values/attrs.xml目录下,也可以直接放在style.xml文件中。
自定义view最重要的就是onMeasure(), onLayout(), onDraw()方法,由于onLayout方法是用来放置子元素的位置,我们这个view没有子元素。所以接着要做的就是重写onMeasure()方法,以及onDraw()方法。通过自定义属性获取字体颜色以及大小设置给Paint,做一下遍历画出所有的字母。view的测量,我这里是通过计算每个字母的宽和高最后叠加计算的。其中有些小细节要注意,比如说我实现的时候发现w和m是要比其他字母稍微宽一点的。
3.最后是重写dispatchTouchEvent
通过计算手指触摸的位置,然后计算出字母的position,然后从字母list中取出相应的字母设置给textview,并通过接口提供出去。
贴出完整的代码:
public class IndexView extends View{
/**
* 索引默认显示的值
*/
private static final int DEFAULT_TEXT_COLOR = Color.BLACK;
private static final int DEFAULT_LINE_SPACE = 24;
private static final int DEFAULT_TEXT_SIZE = 36;
private OnSelectedListener onSelectedListener;
private TextView centerTextView;
private int mTextHeight;
private List mIndexList = new ArrayList<>();
private int mTextSize ;
private int mTextColor ;
private int mLineSpace ;
private Paint mTextPaint;
public IndexView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.IndexView);
try {
mTextColor = a.getColor(R.styleable.IndexView_indexTextColor, DEFAULT_TEXT_COLOR);
mTextSize = a.getDimensionPixelOffset(R.styleable.IndexView_indexTextSize, DEFAULT_TEXT_SIZE);
mLineSpace = a.getDimensionPixelOffset(R.styleable.IndexView_indexLineSpace, DEFAULT_LINE_SPACE);
} finally {
a.recycle();
}
init();
}
private void init() {
/**
* 绘制索引的画笔
*/
mTextPaint = new Paint();
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setAntiAlias(true);
}
public IndexView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setOnSelectedListener(OnSelectedListener listener) {
onSelectedListener = listener;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mIndexList.isEmpty()) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
/**
* Return in bounds (allocated by the caller) the smallest rectangle that
* encloses all of the characters, with an implied origin at (0,0).
*/
Rect rect = new Rect();
mTextPaint.getTextBounds(String.valueOf(mIndexList.get(0)), 0, 1, rect);
mTextHeight = rect.height();
int totalHeight = (mTextHeight + mLineSpace) * mIndexList.size() + getPaddingTop() + getPaddingBottom();
/**
* 这里由于w和M比其他字符宽所以额外加上2dp宽度
* 感觉应该计算
*/
int totalWidth = rect.width() + 2 + getPaddingLeft() + getPaddingRight();
setMeasuredDimension(resolveSize(totalWidth, widthMeasureSpec), resolveSize(totalHeight, totalHeight));
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
for (int i = 0; i < mIndexList.size(); i++) {
canvas.drawText(String.valueOf(mIndexList.get(i)), getPaddingLeft(), calculateIndexY(i), mTextPaint);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int action = event.getAction();
float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
checkSelected(y);
break;
case MotionEvent.ACTION_MOVE:
checkSelected(y);
break;
case MotionEvent.ACTION_UP:
if (centerTextView != null) {
centerTextView.setVisibility(View.INVISIBLE);
}
break;
default:
break;
}
return true;
}
private void checkSelected(float y) {
if (onSelectedListener == null) {
return;
}
if (y < getPaddingTop() || y > (getMeasuredHeight() - getPaddingBottom())) {
return;
}
//选中的字符位置
int position = (int) ((y - getPaddingTop()) / (mTextHeight + mLineSpace));
if (centerTextView != null) {
centerTextView.setText(mIndexList.get(position));
centerTextView.setVisibility(VISIBLE);
}
if (onSelectedListener != null) {
onSelectedListener.onSelected(mIndexList.get(position));
}
invalidate();
}
private int calculateIndexY(int i) {
return getPaddingTop() + mTextHeight * (i + 1) + mLineSpace * i;
}
/**
* 设置IndexView要显示的索引
* @param indexList
*/
public void setIndexList(List indexList) {
if (null == indexList) {
return;
}
mIndexList = indexList;
invalidate();
}
/**
* 用于将外界的textview设置进来,控制其显示
* @param view
*/
public void setCenterTextView(TextView view) {
centerTextView = view;
}
/**
* 对外接口,将indexview选中的事件传递出去
*/
interface OnSelectedListener {
void onSelected(String s);
}
}
上面实现了一个简单的自定义view的功能 ,下面介绍一下自定义ViewGroup。就是如下图所示的电话号码和密码输入的文本框。
1.同样是先分析一下要实现的功能
这个功能就是简单的输入内容,当内容为空的时候不显示清除按钮,当内容为空的时候显示。然后可以设置文本类型,显示提示的hint,就和普通的edittext一样。本来这个东西很简单,就可以用一个edittext,一个用于清除的imageview,一条线来实现的。但是项目中有好几个地方都需要用到同样的功能。就直接把他做成了一个自定义viewgroup。
2. viewgroup的显示
一般来说实现组合控件都是继承一个现有的viewgroup,如Linearlayout ,RelativeLayout这些。然后其中的元素我们既可以写在xml中,也可以在代码中new出来。只是在代码中new出来的话,你就要自己来控制每个子view的位置了。这里我直接写在了布局文件中。如下所示:
同样的因为hint, 输入的文本类型都是edittext有的,我们需要通过自定义属性给组合控件中的edittext设置进去,其中inputtype我们其实可以直接引用系统的
2. viewgroup的逻辑处理
这一步比较简单,直接通过edittext的addTextChangedListener监听文本是否显示,然后控制清除的imageview即可。最后通过方法将文本提供出去即可。
最终的代码如下:
public class ClearableLinearlayout extends RelativeLayout{
private final static String TYPE_NUMBER = "number";
private final static String TYPE_PASSWORD = "password";
private final static String TYPE_PHONE = "phone";
private ImageView clear;
private EditText editText;
private String inputType = TYPE_PHONE;
private String hint = "";
CharSequence beforeText;
public ClearableLinearlayout(Context context) {
super(context);
initView(context);
}
public ClearableLinearlayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ClearableLinearLayout);
inputType = a.getString(R.styleable.ClearableLinearLayout_clearInputType);
hint = a.getString(R.styleable.ClearableLinearLayout_clearHint);
a.recycle();
initView(context);
}
private void initView(Context context) {
LayoutInflater.from(context).inflate(R.layout.clearable_linearlayout, this, true);
editText = findViewById(R.id.edit_content);
clear = findViewById(R.id.ic_clear);
setupActions();
}
private void setupActions() {
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
beforeText = s.toString();
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
clear.setVisibility(s.length() == 0 ? View.INVISIBLE : View.VISIBLE);
if (StringUtil.length(s) > 20) {
editText.setText(beforeText);
editText.setSelection(beforeText.length());
}
}
});
editText.setHint(hint);
if (TYPE_NUMBER.equals(inputType)) {
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
} else if (TYPE_PASSWORD.equals(inputType)) {
editText.setTransformationMethod(PasswordTransformationMethod.getInstance());
} else if (TYPE_PHONE.equals(inputType)){
editText.setInputType(InputType.TYPE_CLASS_PHONE);
}
clear.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
editText.setText("");
}
});
}
public String getContentString() {
return editText.getText().toString().trim();
}
public void setContentString(String string) {
editText.setText(string);
editText.setSelection(string.length());
}
}