来自小白的突突突。。。。。。。。。。。。。。。。。。。。。。。
废话不多说,直接进主题吧
在Android中,控件大致分为两类,ViewGroup和View。
public class LinearLayout extends ViewGroup(线性布局是继承ViewGroup的)
public class RelativeLayout extends ViewGroup(相对布局是继承ViewGroup的)
public class Button extends TextView (按钮继承View)
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener(可以看到TextView也是继承于View)
这里只是列举几个列子,更多的可以自己去查看
由此可以看到,大多控件都是继承ViewGroup和View实现的
View:
那么系统是如何绘制这些View的?
相信大家都曾经玩过这样游戏,一个人蒙着眼睛,拿笔去画板上画一个指定的图案,另一个人则通过说话来
指导他如何去画,比如你会指导他,在距画板边缘10cm的地方画一条边长为5cm的正方形,而如果你只告诉他
画一个矩形,那么你的玩伴就无法精确地画出这个图案了,Android就好像那个蒙着眼睛的人,你必须精确告诉它该如何画,他才会画出你想要的图形。
同样的,我们要去画一个图形,就必须知道他的大少和位置,Android系统在绘制View前,也必须对View进行测量,即告诉Android系统要画多大的View,这个过程就在onMeasure中进行
(这段话是我在“Android群英传”看到的)
我们来新建一个类MyView,继承View,首先实现构造方法,然后重写onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法。代码如下
public class MyView extends View{
public MyView(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
然后按住ctrl鼠标左键,查看父类View的onMeasure();
代码如下
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
这个方法就是拿来测量的,即告诉Android系统要画多大的View。
我们来实现自己的测量
、
Android系统给我们提供了一个MeasureSpec类,来帮助我们测量View,MeasureSpec是一个32位的int值
,其中高两位为测量的模式,低32位为测量的大少。
测量模式可以分为三种
EXACTLY
控件的layout_width,和layout_height设置为具体数值或者match_parent的时候
AT_MOST
即最大值,当控件的layout_widht和layout_height设置为wrap_content的时候,
其控件大少一般随着控件的子控件或内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可
UNSPECIFIED
这个属性比较奇怪,他不指定测量模式,View想多大就多大
自定义控件如果不重写onMeasure(),那默认的只是支持EXACTILY模式,控件可以响应你指定的具体的宽高值或者match_parent,如果设置wrap_content就要重写onMeasure()方法来指定wrap_content时的大小了
下面是自定义控件的代码
public class MyView extends View{
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getMeasuredWidth(widthMeasureSpec);
int height = getMeasureHeight(heightMeasureSpec);
setMeasuredDimension(width,height);
}
//测量宽度
protected int getMeasuredWidth(int widthMeasureSpec){
int result = 0 ;
//这里就以measureWidth()为列子,讲解如何自定义侧值
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
if(widthSpecMode == MeasureSpec.EXACTLY){
//当设置控件width为具体数值,或者match_parent属性时
result = widthSpecSize;
}else{
//否则为AT_MOST或UNSPECIFIED
//UNSPECIFIED模式比较奇怪,他不设定控件大少模式
result = 200; //px
if(widthSpecMode==MeasureSpec.AT_MOST){
//width 设置为wrap_content的时候
result = Math.min(result,widthSpecSize);
}
}
return result;
}
//测量高度,和宽度测量雷同
protected int getMeasureHeight(int heightMeasureSpec){
int result = 0 ;
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if(heightSpecMode == MeasureSpec.EXACTLY){
result = heightSpecSize;
}else{
result = 200; //px
if(heightSpecMode==MeasureSpec.AT_MOST){
result = Math.min(result,heightSpecSize);
}
}
return result;
}
}
然后在布局文件中使用该控件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.ityingli.www.myapplication.MyView
android:background="#000000"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
这个自定义控件设置成黑色,这是铺满全屏的,对应的模式是EXACTLY 的match_parent
修改宽和高的大小
<com.ityingli.www.myapplication.MyView
android:background="#000000"
android:layout_width="100dp"
android:layout_height="200dp" />
对应的模式为 EXACTLY具体值
再猜修改自定义的宽和高
修改成wrap_content
<com.ityingli.www.myapplication.MyView
android:background="#000000"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
对应的是AT_Most模式,默认是被设置成200的
这样就完成了自定义控件的测量了(已经告诉Android绘制控件的大少)
下面来看看自定义控件的绘制,回到我们自定义控件类中
重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
cavas是画布的意思,既然画布有了,要有笔才可以画
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//new 一支笔,一般情况下,不会再这里new Pain(),为简单起见,所以在这里new
Paint paint = new Paint();
//设置笔的属性
//白色
paint.setColor(Color.parseColor("#ffffff"));
//线宽
paint.setStrokeWidth(3);
//画出来的线条是实心的
paint.setStyle(Paint.Style.FILL);
//设置抗锯刺
paint.setAntiAlias(true);
//画一条线
// 参数:startX:起始端点的X坐标。startY:起始端点的Y坐标。
// stopX:终止端点的X坐标。stopY:终止端点的Y坐标.paint:绘制直线所使用的画笔。
canvas.drawLine(0,0,width,height,paint);//width和height分别是控件的宽和高
}
这样我们就完成了绘制和测量,但大家发现了我们的笔的颜色和大少等都是在java代码中设置的。
看看Android系统的设置控件的高度和宽度都是可以在布局中设置的
android:layout_width="wrap_content"
android:layout_height="wrap_content"
下面我们来模拟一下这种实现(自定义属性)
自定义属性步骤:
1.)在res/values文件下添加一个attrs.xml文件,如果项目比较大的话,会导致attrs.xml代码相当庞大,这时可以根据相应的功能模块起名字,方便查找,例如:登录模块相关attrs_test.xml
2.)如何声明一组属性使用<declare-styleable name="PercentView"></declare-styleable>来定义一个属性集合,name就是属性集合的名字,这个名字一定要起的见名知意。
format表示这个属性的值的类型,类型有以下几种:
reference:引用资源
string:字符串
Color:颜色
boolean:布尔值
dimension:尺寸值
float:浮点型
integer:整型
fraction:百分数
enum:枚举类型
flag:位或运算
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyView_linxin"> <!--使用declare-styleable来声明自定义属性-->
<attr name="MyViewColor" format="color"></attr> <!--自定义属性:笔的颜色-->
<attr name="MyViewPaintWidth" format="integer"></attr> <!--自定义属性:笔线的宽度-->
</declare-styleable>
</resources>
3)在布局中使用自定义属性(要声明自定义属性的命名空间)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:MyView="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<!--
上面是我们自己声明的命名控件
xmlns:MyView="http://schemas.android.com/apk/res-auto"
-->
<!--
在下面使用自定义属性
-->
<com.ityingli.www.myapplication.MyView
android:background="#000000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
MyView:MyViewColor="@color/colorPrimary"
MyView:MyViewPaintWidth="3"
/>
</LinearLayout>
4)自定义控件获取自定义属性
每一个属性集合编译之后都会对应一个styleable对象,通过styleable对象获取TypedArray typedArray,然后通过键值对获取属性值,这点有点类似SharedPreference的取法。
在自定义控件的构造方法中获取自定义属性
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.MyView_linxin);
paintColor = typedArray.getColor(R.styleable.MyView_linxin_MyViewColor,Color.parseColor("#ffffff"));
paintwidth = typedArray.getColor(R.styleable.MyView_linxin_MyViewPaintWidth,3);
}
最后把自定义属性设置给paint即可
//笔的颜色
paint.setColor(paintColor);
//线宽
paint.setStrokeWidth(paintwidth);
效果图:
下面为完整代码,MainActivity不用修改
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
MainActivity对应的布局
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:MyView="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" > <com.ityingli.www.myapplication.MyView android:background="#000000" android:layout_width="100dp" android:layout_height="200dp" MyView:MyViewColor="#3F51B5" MyView:MyViewPaintWidth="3" /> </LinearLayout>
/** * Created by Administrator on 2017/7/14. */ public class MyView extends View{ int width = 0; int height = 0 ; //笔的颜色,笔线的宽度 int paintColor ; int paintwidth; public MyView(Context context) { this(context,null); } public MyView(Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //获取自定义属性 TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.MyView_linxin); paintColor = typedArray.getColor(R.styleable.MyView_linxin_MyViewColor,Color.parseColor("#ffffff")); paintwidth = typedArray.getColor(R.styleable.MyView_linxin_MyViewPaintWidth,3); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { width = getMeasuredWidth(widthMeasureSpec); height = getMeasureHeight(heightMeasureSpec); setMeasuredDimension(width,height); } //测量宽度 protected int getMeasuredWidth(int widthMeasureSpec){ int result = 0 ; //这里就以measureWidth()为列子,讲解如何自定义侧值 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); if(widthSpecMode == MeasureSpec.EXACTLY){ result = widthSpecSize; }else{ result = 200; //px if(widthSpecMode==MeasureSpec.AT_MOST){ result = Math.min(result,widthSpecSize); } } return result; } //测量高度,和宽度测量雷同 protected int getMeasureHeight(int heightMeasureSpec){ int result = 0 ; int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if(heightSpecMode == MeasureSpec.EXACTLY){ result = heightSpecSize; }else{ result = 200; //px if(heightSpecMode==MeasureSpec.AT_MOST){ result = Math.min(result,heightSpecSize); } } return result; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); paint.setColor(paintColor); paint.setStrokeWidth(paintwidth); paint.setStyle(Paint.Style.FILL); paint.setAntiAlias(true); // 参数:startX:起始端点的X坐标。startY:起始端点的Y坐标。 // stopX:终止端点的X坐标。stopY:终止端点的Y坐标.paint:绘制直线所使用的画笔。 canvas.drawLine(0,0,width,height,paint);//width和height分别是控件的宽和高 } }
自定义控件中获取的自定义属性(res/values/attrs.xml)
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyView_linxin"> <!--使用declare-styleable来声明自定义属性--> <attr name="MyViewColor" format="color"></attr> <!--自定义属性:笔的颜色--> <attr name="MyViewPaintWidth" format="integer"></attr> <!--自定义属性:笔线的宽度--> </declare-styleable> </resources>