初探自定义控件

作为移动开发,自定义控件必不可少,之前大多时候用的自定义控件都是在网上复制粘贴的,并没有仔细琢磨,趁着有时间,尝试自己写写看,并把遇到的问题总结一下!

一般来说把自定义控件分为3中:1、自绘控件(自己绘制各种图形) 2、组合控件(系统已有控件组合到一起) 3、继承控件(对系统已有控件功能扩展)

首先了解view绘制的三个主要流程,Measure----Layout----Draw

View先测量大小,对应的方法是onMeasure();设置子控件的位置,对应方法是onLayout();绘制图形,对应的方法是onDraw();

一、自绘控件

绘制一个简单的图形: 一个太极图标

首先新建类继承View,重写起构造方法,与我们紧密相关的主要有三个方法:onMeasure(),draw(),onTouchEvent();

在onMeasure方法中主要绘制控件的大小,在这个方法中可以设置空间的大小,其中需要注意的是MeasureSpec.getSize()方法,可以获取控件的大小。



@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, widthMeasureSpec);//设置宽度与高度相等
    viewWidth=MeasureSpec.getSize(widthMeasureSpec);//控件的宽度  指的是像素
    Log.e("myview", "onMeasure: ----size----" + MeasureSpec.getSize(widthMeasureSpec)
            + "----mode---" + MeasureSpec.getMode(widthMeasureSpec));
}

在draw()方法中绘制图形,绘制图形主要有Paint(画笔),Canvas(画布)两个类完成的:

有两种黑白颜色的画笔,定义一个创建得到画笔的方法:

private Paint getPaint(int color){
    Paint paint=new Paint();
    paint=new Paint();
    paint.setAntiAlias(true);////设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢。
    paint.setDither(true);//设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰

    /*
    * 置画笔样式,如果不设置,默认是全部填充(FILL)。可选项为:FILLFILL_OR_STROKE,或STROKE
        画笔样式分三种:
            1.Paint.Style.STROKE:描边
            2.Paint.Style.FILL_AND_STROKE:描边并填充
            3.Paint.Style.FILL:填充
    * */
    paint.setStyle(Paint.Style.FILL_AND_STROKE);
    paint.setColor(color);
    return paint;
}
然后再onDraw()方法中绘制图形

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    int srccnWidth=viewWidth;

    //控制椭圆大小及位置的矩形坐标  对应的是左上 右下坐标  这里表示的一个正方形  绘制出来的是一个圆形圆弧
    RectF rect=new RectF(0,0,srccnWidth,srccnWidth);
    //左侧黑色圆弧
    canvas.drawArc(rect,90,180,false,mArcPaintBlack);//false 表示只绘制一个圆弧   true表示 圆弧将会闭合 会多出来一条线
    //右侧白色圆弧
    canvas.drawArc(rect,-90,180,false,mArcPaintWhite);

    //上方黑色小圆弧
    RectF rect2=new RectF(srccnWidth/4,0,srccnWidth/4*3,srccnWidth/2);
    canvas.drawArc(rect2,-90,180,false,mArcPaintBlack);
    //下方白色小圆弧
    RectF rect3=new RectF(srccnWidth/4,srccnWidth/2,srccnWidth/4*3,srccnWidth);
    canvas.drawArc(rect3,90,180,false,mArcPaintWhite);

    //黑色圆圈
    canvas.drawCircle(srccnWidth/2,srccnWidth/4,srccnWidth/16,mArcPaintWhite);
    //白色圆圈
    canvas.drawCircle(srccnWidth/2,srccnWidth/4*3,srccnWidth/16,mArcPaintBlack);
}

在自定义控件中添加自定义的属性(如控制画笔的颜色):

在valus目录下创建attrs.xml文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="myView">
        <attr name="lbb_painColor" format="color|reference"/>
        <attr name="lbb_lineWeigh" format="dimension"/>
    </declare-styleable>
</resources>
name="lbb_viewbg"   自定义属性名字    format="color|reference"   属性类型

format各种值的含义:

reference:引用资源
string:字符串
Color:颜色
boolean:布尔值
dimension:尺寸值
float:浮点型
integer:整型
fraction:百分数
enum:枚举类型
flag:位或运算

在自定义控件中获取属性:

public MyView2(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    getProperties(attrs);//在构造方法执行
}

//获取自定义属性的值
private void getProperties(AttributeSet attrs){
    TypedArray typedArray=getContext().obtainStyledAttributes(attrs, R.styleable.myView);
    colorWhite=typedArray.getColor(R.styleable.myView_lbb_painColor, colorWhite);//获取画笔颜色 属性
    mLineStrokeWidth=typedArray.getDimension(R.styleable.myView_lbb_lineWeigh, mLineStrokeWidth);//获取画笔大小
    typedArray.recycle();
    
    mArcPaintBlack=getPaint(colorBlack);
    mArcPaintWhite=getPaint(colorWhite);//给黑色画笔设置颜色
}

在xml中定义属性:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:lbb="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:layout_margin="20dp"
    >
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            >

            <com.example.teemo.myview_demo.myView.MyView2
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="#00ff00"
                lbb:lbb_painColor="#ffff00"
                />
            <View
                android:layout_width="match_parent"
                android:layout_height="20dp"/>
            <com.example.teemo.myview_demo.myView.MyView1
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="#ff0000"
                />
        </LinearLayout>
    </ScrollView>
</LinearLayout>

在布局文件中根控件中添加

xmlns:lbb="http://schemas.android.com/apk/res-auto"  其中lbb自己任意定义  据说添加这个后Gradle自动查找自定义的属性(没有深入研究)

在自定义控件MyView2中添加属性

lbb:lbb_painColor="#ffff00"   这时白色的画笔会变为我们设置的颜色

初探自定义控件

另外有有一点,当绘制线条时,如果线条很粗,要考虑我们定义的画笔开始/结束的坐标位于线条的中心,如当我们定义的线条宽度为80f时,在空间左边缘竖直画一条线时,其宽度只有40f,

以为还有一半超出了控件边界。如

canvas.drawLine(0,0,0,mLineStrokeWidth,mLinePaint);

可以改为 canvas.drawLine(40,0,40,mLineStrokeWidth,mLinePaint);

这个是画万字符号时遇到的问题

二 、组合控件

组合控件只要把系统已有的控件组合起来即可,实现相对复杂的布局,也可以把常用到的布局封装起来。

以linearlayout 中嵌套  TextView  和 Recyclerview  为例,其效果如下

初探自定义控件

首先定义一个类在其内部实现布局:

/**
 * Created by lianbinbo on 2016/12/20.
 */
public class MyViewHolder extends RelativeLayout {
    private Context mContext;
    private RecyclerView mRecyclerView;
    private TextView mTextView;
    private Adapter_view3 mAdapter_view3;

    public MyViewHolder(Context context) {
        super(context);
        this.mContext=context;
        initView();
    }

    private void initView(){
        View view= LayoutInflater.from(mContext).inflate(R.layout.myview3,this);//要实现的布局
        mRecyclerView= (RecyclerView) view.findViewById(R.id.rv_item);
        mTextView= (TextView) view.findViewById(R.id.tv_title);

        GridLayoutManager gridLayoutManager=new GridLayoutManager(mContext,2);//实现recycleview为网格形式
        if (mAdapter_view3==null){
            mAdapter_view3=new Adapter_view3(mContext);
        }
        if (mRecyclerView!=null){
            mRecyclerView.setLayoutManager(gridLayoutManager);
            mRecyclerView.setHasFixedSize(true);
            mRecyclerView.setAdapter(mAdapter_view3);
        }

    }
    public void addItem(List<ItemModle> itemModle,int pos){//得到数据
        if (mAdapter_view3!=null){
            mAdapter_view3.updateItems(itemModle);//更新界面
            Log.e("myView3", "myViewHolder: -----" + itemModle.size() + "---pos---" + pos);
        }
        if (mTextView!=null){//如果要增加列表 直接增加数据 在这里修改列表头 在适配器 判断显示不同数据
            if (pos==0){
                mTextView.setText("质态");
            }else {
                mTextView.setText("资源");
            }
        }
    }
}

该demo使用该自定义布局是在Recycleview中,在一个Activity中定义一个RecyclerView,然后把该自定义控件加载到Recycleview中,可以实现上面的排列效果,如果要增加排列的数量,直接控制数据就可以了。

Activity 中布局:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.teemo.myview_demo.Main2Activity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_main2Activity"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

Activity中RecyclerView中的适配器:

/**
 * Created by lianbinbo on 2016/12/20.
 */
public class MyAdapter_main2 extends BaseRecyclerAdapter<MyModle1> {
    public MyAdapter_main2(Context mContext) {
        super(mContext);
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        RecyclerView.ViewHolder viewHolder;
        viewHolder=new ViewHolderMain2(new MyViewHolder(mContext));
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        ViewHolderMain2 viewHolderMain2= (ViewHolderMain2) holder;
        //把数据传到定义的控件中
        viewHolderMain2.myViewHolder.addItem(mItem.get(position).getItemModleList(),position);
        Log.e("myView3", "onBindViewHolder--1--: --position---"+position+"---size---"+mItem.size() );
    }

    public class ViewHolderMain2 extends RecyclerView.ViewHolder{
        MyViewHolder myViewHolder;//自己定义的控件
        public ViewHolderMain2(View itemView) {
            super(itemView);
            myViewHolder= (MyViewHolder) itemView;
        }
    }
}

自定义控件中的适配器:

/**
 * Created by lianbinbo on 2016/12/20.
 */
public class Adapter_view3 extends BaseRecyclerAdapter<ItemModle> {

    public Adapter_view3(Context mContext) {
        super(mContext);
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new ViewHolderView3(LayoutInflater.from(mContext).inflate(R.layout.myview3_item,parent,false));
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        ViewHolderView3 viewHolderView3= (ViewHolderView3) holder;
        ItemModle itemModle=getItem(position);
        if ("0".equals(itemModle.getTag())){//根据  传入的标记  显示不同的图片
            viewHolderView3.iv.setImageResource(R.drawable.home_grid_04);
        }else if ("1".equals(itemModle.getTag())){
            viewHolderView3.iv.setImageResource(R.drawable.home_grid_06);
        }
        viewHolderView3.textView.setText(itemModle.getName());
        Log.e("myView3", "onBindViewHolder--2--: ---position--" + position + "---size---" + mItem.size());
    }

    public class ViewHolderView3 extends RecyclerView.ViewHolder{
        private ImageView iv;
        private TextView textView;
        public ViewHolderView3(View itemView) {
            super(itemView);
            iv= (ImageView) itemView.findViewById(R.id.iv);
            textView= (TextView) itemView.findViewById(R.id.tv_item);
        }
    }
}

这样把控件封装到一起,有类似的布局就可以复用:而且在Recycleview中用该控件,可以轻松实现有连续相似布局的效果。像一些视频APP节目的展示:

初探自定义控件

三,继承控件,这个不多说了,直接在网上复制一个代码,实现禁止GridView滑动的效果

public class GridViewInScrollView extends GridView {

    public GridViewInScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public GridViewInScrollView(Context context) {
        super(context);
    }

    public GridViewInScrollView(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

}