目录
- 声明
- 1.继承自ViewGroup
- 2.定义获取相关属性->res/values/attrs
- 3.把子view加载进来
- 4.测量自己和子View的大小
- MeasureSpec的三种模式
- 精确模式MeasureSpec.EXACTLY:
- 最大模式(MeasureSpec.AT_MOST):
- 未指定模式(MeasureSpec.UNSPECIFIED):
- 5.摆放子View
- 6.定义功能接口 interface
- 7.暴露点击事件供外部实现
- 8.将属性设置到内容中
声明
本文章例子来源于bilibili up 程序猿拉大锯 本文章主要是自己对于一些知识点理解算是总结吧,记录自己的学习
本专栏不会进行收费,所有文章都会设置成试读,请各位读者放心,设置收费专栏是为了让更多的人看到此专栏
欧克,从前两篇文章我们已经熟悉了自定义组合控件的操作流程,
1.继承自父容器
2.定义获取相关属性->res/values/attrs
3.把子view加载进来
4.定义功能接口 interface
5.暴露点击事件供外部实现
6.将属性设置到内容中
根据操作流程我们就可以很简单的进行自定义组合控件的编写了
从本章内容开始,我们才算是真正的开始接触自定义View的内容啦,我们上面的文章也提到过说自定义View的控件类型分为自定义组合控件/自定义View/和自定义ViewGroup 今天我们就来啃硬骨头 先从自定义ViewGroup开始讲起
自定义ViewGroup的操作流程
1.继承自ViewGroup
2.定义获取相关属性->res/values/attrs
3.把子view加载进来
4.测量自己和子View的大小
5.摆放子View
6.定义功能接口 interface
7.暴露点击事件供外部实现
8.将属性设置到内容中
首先我们也来简单了解一下ViewGroup的生命周期:
构造方法->onFinishInflate()->onMeasure()->onSizeChanged()->onLayout()
构造方法:拿到自定义ViewGroup的实例
onFinishInflate():布局加载完成回调
onMeasure():测量子View,测量自己
onSizeChanged():在onMeasure()后面执行,当大小放生改变会进行回调
onLayout():摆放子View
欧克,废话不多说直接开始我们的正文:
1.继承自ViewGroup
public class LoginPadView extends ViewGroup {
public LoginPadView(Context context) {
this(context,null);
}
public LoginPadView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
//统一入口
public LoginPadView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
欧克,这里呢我们新建了LoginPadView类继承自ViewGroup 它会让我们强制实现onLayout()方法,因为我们继承的是父容器,容器内可以包含多个子View或者父容器,我们在onLayout方法中要将我们准备显示的子View进行摆放,这里可能会有同学有疑问,在前两章自定义组合控件我们继承的父容器不是直接或间接继承自ViewGroup么,为什么我们这里直接继承ViewGroup还要重写
onLayout方法呢?
因为我们继承的父容器他都已经将onLayout实现过了,我们的组合控件相当于在父容器的基础上进行修改了一点点 ,哪有什么岁月静好,只不过有人替你负重前行
2.定义获取相关属性->res/values/attrs
和之前自定义组合控件的流程一样,这里我们在res/values/attrs下的resources标签下创建我们要用的到属性
<declare-styleable name="LoginPadView">
<attr name="itemMargin" format="dimension"/>
<attr name="numberColor" format="color"/>
<attr name="numberSize" format="dimension"/>
<attr name="itemPressColor" format="color"/>
<attr name="itemNormalColor" format="color"/>
</declare-styleable>
在代码中回去定义好的属性
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LoginPadView);
mColor = a.getColor(R.styleable.LoginPadView_numberColor, context.getResources().getColor(R.color.itemColor));
//设置成px
mSize = a.getDimensionPixelSize(R.styleable.LoginPadView_numberSize, -1);
mPressBg = a.getColor(R.styleable.LoginPadView_itemPressColor, context.getResources().getColor(R.color.itemPress));
mNormalBg = a.getColor(R.styleable.LoginPadView_itemNormalColor, context.getResources().getColor(R.color.itemNormal));
//dp ->px
itemMargin = a.getDimensionPixelSize(R.styleable.LoginPadView_itemMargin, SizeUtils.dip2px(2));
//关闭
a.recycle();
}
获取玩属性后记得要关闭哟
3.把子view加载进来
private void setUpItem() {
removeAllViews();//清空子View
for (int i = 0; i < 11; i++) {
TextView textView = new TextView(getContext());
if(i==10){
textView.setTag(true);
textView.setText("删除");
}else {
textView.setTag(false);
textView.setText(String.valueOf(i));
}
//setTextSize一个参数时单位默认是sp
//设置背景
textView.setBackground(providerItemBg());
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
}
});
addView(textView);//添加子private void setUpItem() {
}
//设置按下去的效果和圆角 这里的效果就和上一篇文章新建Drawable是一样的 只不过这里使用代码生成
private Drawable providerItemBg() {
//按下去bg
GradientDrawable pressDrawable = new GradientDrawable();
pressDrawable.setColor(getResources().getColor(R.color.itemPress));
pressDrawable.setCornerRadius(SizeUtils.dip2px(5));
StateListDrawable bg = new StateListDrawable();
bg.addState(new int[]{android.R.attr.state_pressed},pressDrawable);
//普通状态的bg
GradientDrawable normalDrawable = new GradientDrawable();
normalDrawable.setColor(getResources().getColor(R.color.itemNormal));
normalDrawable.setCornerRadius(SizeUtils.dip2px(5));
bg.addState(new int[]{},normalDrawable);
return bg;
}
这里我们先调用removeAllViews(); 清空容器中的所有View 这里在我们添加时确保容器内没有任何数据,然后我们使用for循环生成了11个TextView,并设置最后一个Text View的文字内容为‘删除’ 并且为了标识删除的这个Text View 我们为Text View设置了一个口袋存放一些数据,textView.setTag(true);
然后我们这里又设置了text View的背景颜色,这里采用代码的方式实现按下、圆角的效果
最后就是将生成的子View添加进来
4.测量自己和子View的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.i("LoginPadView","onMeasure...");
//测量孩子
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//4行三列
int itemWidth = widthSize/3;
int itemHeight = heightSize/4;
for (int i = 0; i <getChildCount() ; i++) {
View item = getChildAt(i);
boolean isDelete = (boolean) item.getTag();
int normalWidth = MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY);
int normalHeight = MeasureSpec.makeMeasureSpec(itemHeight, MeasureSpec.EXACTLY);
int deleteWidth = MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY);
//测量每一个孩子
item.measure(isDelete?deleteWidth:normalWidth,normalHeight);
}
//测量自己
setMeasuredDimension(widthSize,heightSize);
}
这里我们就需要重写onMeasure()根据传过来的layout_width和layout_height来确定子View的尺寸
这里的Measure的两个参数 widthMeasureSpec, heightMeasureSpec都是由两部分组成的,分别是mode和size
这里我们可以通过**MeasureSpec.getSize()/getMode()**来获取宽高的尺寸和模式
getSize()就是获取尺寸,这里也没什么好讲的,主要就是模式我们要来讲一讲
MeasureSpec的三种模式
精确模式MeasureSpec.EXACTLY:
在这种模式下,尺寸的值是多少,那么这个组件的长或宽就是多少。
最大模式(MeasureSpec.AT_MOST):
这个也就是父组件,能够给出的最大的空间,当前组件不能超过父容器给的最大值,当然也可以比这个小。
未指定模式(MeasureSpec.UNSPECIFIED):
子组件想多大就多大 没什么实际意义 在日常开发中不经常使用
这里你可以自己做个小测试,在要显示的布局中分别将我们当前定义的自定义数字键盘根路径放上去,将layout_width/layout_height设置为match_parent/wrap_content/指定数值来看一下返回的结果和尺寸就明了了
来看我们的代码
我们这里使用MeasureSpec.getSize分别获取当前自定义View的宽高
然后设置4行三列,然后使用getChildCount()来获取子View的总个数,然后测量每个子View,这里我们使用boolean isDelete = (boolean) item.getTag();来判断当前View是否为删除的哪一个 ,我们设置这一个独占2份
然后我们调用setMeasuredDimension(widthSize,heightSize); 测量自己
5.摆放子View
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
for (int i = 0; i <getChildCount() ; i++) {
View childAt = getChildAt(i);
//行
int rowIndex = i/3;
//列
int columnIndex = i%3;
Log.i("ilala","@"+columnIndex);
top = rowIndex*childAt.getMeasuredHeight();
right = left+childAt.getMeasuredWidth();
bottom = top+childAt.getMeasuredHeight();
childAt.layout(left,top,right,bottom);
left+=childAt.getMeasuredWidth()+itemMargin;
}
}
通过以上代码我们就可以摆放我们子view的位置,从而根据当前位置进行显示子View
childAt.layout(left,top,right,bottom);进行子View的摆放
6.定义功能接口 interface
这里和上一张内容是一样的 也不多作介绍
public interface onKeyPressListener{
//数字键盘被点击
void onNumberPress(int number);
//删除键被点击
void onDelPress();
}
7.暴露点击事件供外部实现
private onKeyPressListener onKeyPressListener = null;
public void setOnKeyPressListener(onKeyPressListener listener){
onKeyPressListener = listener;
}
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
TextView v1 = (TextView) v;
boolean tag = (boolean) v1.getTag();
if(tag){
onKeyPressListener.onDelPress();
}else {
onKeyPressListener.onNumberPress(Integer.parseInt(v1.getText().toString()));
}
}
});
这里我们根据我们之前传入的setTag值进行判断当前点击的是否是删除键,如果是的话就走我们定义的删除键 否则进入onNumberPress接口中
8.将属性设置到内容中
private void setUpItem() {
removeAllViews();
for (int i = 0; i < 11; i++) {
TextView textView = new TextView(getContext());
if(i==10){
textView.setTag(true);
textView.setText("删除");
}else {
textView.setTag(false);
textView.setText(String.valueOf(i));
}
//setTextSize一个参数时单位默认是sp
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,mSize);
textView.setGravity(Gravity.CENTER);
textView.setTextColor(mColor);
//设置背景
textView.setBackground(providerItemBg());
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
TextView v1 = (TextView) v;
boolean tag = (boolean) v1.getTag();
if(tag){
onKeyPressListener.onDelPress();
}else {
onKeyPressListener.onNumberPress(Integer.parseInt(v1.getText().toString()));
}
}
});
addView(textView);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.i("LoginPadView","onMeasure...");
//测量孩子
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//4行三列
int itemWidth = (widthSize - 4*itemMargin)/3;
int itemHeight = (heightSize-5*itemMargin)/4;
for (int i = 0; i <getChildCount() ; i++) {
View item = getChildAt(i);
boolean isDelete = (boolean) item.getTag();
int normalWidth = MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY);
int normalHeight = MeasureSpec.makeMeasureSpec(itemHeight, MeasureSpec.EXACTLY);
int deleteWidth = MeasureSpec.makeMeasureSpec(itemWidth * 2+itemMargin, MeasureSpec.EXACTLY);
//测量每一个孩子
item.measure(isDelete?deleteWidth:normalWidth,normalHeight);
}
//测量自己
setMeasuredDimension(widthSize,heightSize);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.i("LoginPadView","onLayout...");
int left = itemMargin;
int top = 0;
int right = 0;
int bottom = 0;
for (int i = 0; i <getChildCount() ; i++) {
View childAt = getChildAt(i);
//行
int rowIndex = i/3;
//列
int columnIndex = i%3;
Log.i("ilala","@"+columnIndex);
if(columnIndex==0){
left = itemMargin;
}
top = rowIndex*childAt.getMeasuredHeight()+itemMargin*(rowIndex+1);
right = left+childAt.getMeasuredWidth();
bottom = top+childAt.getMeasuredHeight();
childAt.layout(left,top,right,bottom);
left+=childAt.getMeasuredWidth()+itemMargin;
}
}
欧克到这里我们的自定义数字键盘就算结束了
来看总体的代码
public class LoginPadView extends ViewGroup {
private int mColor;
private float mSize;
private int mPressBg;
private int mNormalBg;
private int itemMargin;
private onKeyPressListener onKeyPressListener = null;
public LoginPadView(Context context) {
this(context,null);
}
public LoginPadView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public LoginPadView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取属性
initAttrs(context, attrs);
//setUpItem
setUpItem();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
Log.i("LoginPadView","onFinishInflate...");
}
private void setUpItem() {
removeAllViews();
for (int i = 0; i < 11; i++) {
TextView textView = new TextView(getContext());
if(i==10){
textView.setTag(true);
textView.setText("删除");
}else {
textView.setTag(false);
textView.setText(String.valueOf(i));
}
//setTextSize一个参数时单位默认是sp
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,mSize);
textView.setGravity(Gravity.CENTER);
textView.setTextColor(mColor);
//设置背景
textView.setBackground(providerItemBg());
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
TextView v1 = (TextView) v;
boolean tag = (boolean) v1.getTag();
if(tag){
onKeyPressListener.onDelPress();
}else {
onKeyPressListener.onNumberPress(Integer.parseInt(v1.getText().toString()));
}
}
});
addView(textView);
}
}
private Drawable providerItemBg() {
//按下去bg
GradientDrawable pressDrawable = new GradientDrawable();
pressDrawable.setColor(getResources().getColor(R.color.itemPress));
pressDrawable.setCornerRadius(SizeUtils.dip2px(5));
StateListDrawable bg = new StateListDrawable();
bg.addState(new int[]{android.R.attr.state_pressed},pressDrawable);
//普通状态的bg
GradientDrawable normalDrawable = new GradientDrawable();
normalDrawable.setColor(getResources().getColor(R.color.itemNormal));
normalDrawable.setCornerRadius(SizeUtils.dip2px(5));
bg.addState(new int[]{},normalDrawable);
return bg;
}
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LoginPadView);
mColor = a.getColor(R.styleable.LoginPadView_numberColor, context.getResources().getColor(R.color.itemColor));
//设置成px
mSize = a.getDimensionPixelSize(R.styleable.LoginPadView_numberSize, -1);
mPressBg = a.getColor(R.styleable.LoginPadView_itemPressColor, context.getResources().getColor(R.color.itemPress));
mNormalBg = a.getColor(R.styleable.LoginPadView_itemNormalColor, context.getResources().getColor(R.color.itemNormal));
//dp ->px
itemMargin = a.getDimensionPixelSize(R.styleable.LoginPadView_itemMargin, SizeUtils.dip2px(2));
a.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.i("LoginPadView","onMeasure...");
//测量孩子
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//4行三列
int itemWidth = (widthSize - 4*itemMargin)/3;
int itemHeight = (heightSize-5*itemMargin)/4;
for (int i = 0; i <getChildCount() ; i++) {
View item = getChildAt(i);
boolean isDelete = (boolean) item.getTag();
int normalWidth = MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY);
int normalHeight = MeasureSpec.makeMeasureSpec(itemHeight, MeasureSpec.EXACTLY);
int deleteWidth = MeasureSpec.makeMeasureSpec(itemWidth * 2+itemMargin, MeasureSpec.EXACTLY);
//测量每一个孩子
item.measure(isDelete?deleteWidth:normalWidth,normalHeight);
}
//测量自己
setMeasuredDimension(widthSize,heightSize);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.i("LoginPadView","onLayout...");
int left = itemMargin;
int top = 0;
int right = 0;
int bottom = 0;
for (int i = 0; i <getChildCount() ; i++) {
View childAt = getChildAt(i);
//行
int rowIndex = i/3;
//列
int columnIndex = i%3;
Log.i("ilala","@"+columnIndex);
if(columnIndex==0){
left = itemMargin;
}
top = rowIndex*childAt.getMeasuredHeight()+itemMargin*(rowIndex+1);
right = left+childAt.getMeasuredWidth();
bottom = top+childAt.getMeasuredHeight();
childAt.layout(left,top,right,bottom);
left+=childAt.getMeasuredWidth()+itemMargin;
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.i("LoginPadView","onSizeChanged...");
}
public void setOnKeyPressListener(onKeyPressListener listener){
onKeyPressListener = listener;
}
public interface onKeyPressListener{
//数字键盘被点击
void onNumberPress(int number);
//删除键被点击
void onDelPress();
}
}
这里的效果和上一张的效果是一样的 就不展示效果图了 我们的自定义的数字键盘的特点就是可以根据用户输入的大小进行屏幕适配,并且有自己的属性
欧克,到这里我们的第一篇自定义内容就算是结束了 这里再摆放我们的子View的时候涉及到一点数学运算 其他也不是很难 多练练相信你也会跟上我们的节奏的
记得跟着步骤走
下一篇文章也是关于自定义ViewGroup的 可以帮助你巩固所学的知识 自定义View设计的内容还是比较广的 且比较复杂 平时一定要多练哟