Android简单了解自定义控件和自定义属性如何使用

时间:2021-02-20 20:38:20

来自小白的突突突。。。。。。。。。。。。。。。。。。。。。。。

废话不多说,直接进主题吧




在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>

Android简单了解自定义控件和自定义属性如何使用

这个自定义控件设置成黑色,这是铺满全屏的,对应的模式是EXACTLY 的match_parent



修改宽和高的大小
<com.ityingli.www.myapplication.MyView
      android:background="#000000"
      android:layout_width="100dp"
      android:layout_height="200dp" />

Android简单了解自定义控件和自定义属性如何使用


对应的模式为 EXACTLY具体值


 再猜修改自定义的宽和高

修改成wrap_content
  <com.ityingli.www.myapplication.MyView
      android:background="#000000"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content" />
Android简单了解自定义控件和自定义属性如何使用

对应的是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分别是控件的宽和高
    }

Android简单了解自定义控件和自定义属性如何使用


这样我们就完成了绘制和测量,但大家发现了我们的笔的颜色和大少等都是在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);


效果图:

Android简单了解自定义控件和自定义属性如何使用



下面为完整代码,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>


布局中的自定义控件MyView

/**
* 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);//widthheight分别是控件的宽和高
}
}


自定义控件中获取的自定义属性(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>