自定义View

这篇文章分两方面描述Android中的自定义view,分别是自定义view和自定义viewGroup,它们又可以分为继承view(或viewgroup)和继承已有控件。

关于自定义view,涉及到很多图形方面的知识,其中用的最多的是paint和canvas。其api较多,这里不写出,请去官网查询:graphic

1. 自定义view

  • 继承已有控件
    这里实现一个左上角带有图片说明的图片。效果如图效果图1

实现过程也很简单,onDraw方法中在图片上绘制一个梯形角标一个,在角标上绘制文字,就OK了。代码

public class MyimageView extends ImageView {

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

    public MyimageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //设置画笔
        Paint  mpanit=new Paint();
        mpanit.setColor(Color.YELLOW);
        mpanit.setStyle(Paint.Style.FILL);//绘制类型为填充
        

        //找出宽高中最小边.
        int minEdge=getWidth()>getHeight()?getHeight():getWidth();

        Path mpath=new Path();
        //view绘制的时候坐标(0,0)在左上方
        // 以下几步画一个梯形
        mpath.moveTo(0,minEdge/4);//移动路径起点
        mpath.lineTo(0,minEdge/2);//画线
        mpath.lineTo(minEdge/2,0);
        mpath.lineTo(minEdge/4,0);
        mpath.close();//把开口的路径关闭。终点与原点相连。
        //绘制这个梯形
        canvas.drawPath(mpath,mpanit);

        Paint  textpanit=new Paint();
        textpanit.setColor(Color.RED);
        textpanit.setTextSize(30);
        canvas.rotate(-45,getWidth()/2,getHeight()/2);//画布以图片中心逆时针旋转45度
        canvas.drawText("图片说明",getWidth()/4+getHeight()/11,getHeight()/11,textpanit);
    }
}

要注意view绘制的原点在左上角。使用该自定义view。

<com.example.mycustomview.MyimageView
    android:layout_width="150dp"
    android:layout_height="150dp"
    android:layout_gravity="center"
    android:layout_marginTop="50dp"
    android:src="@drawable/show"
    android:background="#13ea41"/>
  • 继承view
    这里通过自定义实现一个类似于扇形统计图的东西,只是一个很粗略的,绘制了界面带点动画而已,毕竟练练手。如图show2
    做法也跟之前的一样同样是绘制就行了。只不过这里是绘制了扇形。然后在图形的基础上了加了一些动画。代码如下
public class MyView extends View{
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint1=new Paint();
        paint1.setStyle(Paint.Style.FILL);
        paint1.setColor(Color.RED);

        Paint paint2=new Paint();
        paint2.setStyle(Paint.Style.FILL);
        paint2.setColor(Color.YELLOW);

        Paint paint3=new Paint();
        paint3.setStyle(Paint.Style.FILL);
        paint3.setColor(Color.BLUE);

        float radius=Math.min(getWidth(),getHeight())/2;
        RectF rectF=new RectF(0,0,radius*2,radius*2);

        //drawArc方法第一个参数(矩形)限定画圆的范围,第二个是起始角度,第三个是绘制角度
        //第四个参数发false画圆弧,true画扇形,第五个参数是画笔。
        canvas.drawArc(rectF,0,120,true,paint1);
        canvas.drawArc(rectF,120,120,true,paint2);
        canvas.drawArc(rectF,240,120,true,paint3);

        startAnimator();
    }

    //view伸缩动画
    private void startAnimator(){
        //设置缩放原点
        this.setPivotX(this.getWidth()/2);
        this.setPivotY(this.getHeight()/2);

        //组合动画缩放
        AnimatorSet animatorSet = new AnimatorSet();
        ObjectAnimator scaleX=ObjectAnimator.ofFloat(this,"scaleX",1f,1.6f,1f);
        ObjectAnimator scaleY=ObjectAnimator.ofFloat(this,"scaleY",1f,1.6f,1f);
        //scaleX.setRepeatMode(ObjectAnimator.RESTART);
        //scaleY.setRepeatMode(ObjectAnimator.RESTART);
        scaleX.setRepeatCount(ObjectAnimator.INFINITE);//无限循环播放
        scaleY.setRepeatCount(ObjectAnimator.INFINITE);
        scaleX.setDuration(800);
        scaleY.setDuration(800);
        animatorSet.play(scaleX).with(scaleY);
        animatorSet.start();
    }
}

当然这里还有一种动画的实现方式那就是多次ondraw绘制,比如我想做一个像扇形打开的那种扇形绘制动画,就分10次绘制,每次多绘制扇形的1/10。下面是效果图(不会截gif图,所以截了一张过程图)和代码show3

int times=0;
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Paint paint1=new Paint();
    paint1.setStyle(Paint.Style.FILL);
    paint1.setColor(Color.RED);

    Paint paint2=new Paint();
    paint2.setStyle(Paint.Style.FILL);
    paint2.setColor(Color.YELLOW);

    Paint paint3=new Paint();
    paint3.setStyle(Paint.Style.FILL);
    paint3.setColor(Color.BLUE);

    float radius=Math.min(getWidth(),getHeight())/2;
    RectF rectF=new RectF(0,0,radius*2,radius*2);

    //drawArc方法第一个参数(矩形)限定画圆的范围,第二个是起始角度,第三个是绘制角度
    //第四个参数发false画圆弧,true画扇形,第五个参数是画笔。
    canvas.drawArc(rectF,0,120*times/10,true,paint1);
    canvas.drawArc(rectF,120,120*times/10,true,paint2);
    canvas.drawArc(rectF,240,120*times/10,true,paint3);

    times++;
    if(times<=10){
        postDelayed(new Runnable() {
            @Override
            public void run() {
                invalidate();//通知onDraw方法再次绘制
            }
        },300);
    }
}

用这个view如下。

<com.example.mycustomview.MyView
    android:layout_width="150dp"
    android:layout_height="150dp"
    android:layout_gravity="center"
    android:layout_marginTop="50dp"/>

这里发现一个问题。如果这个view的长宽设置成wrap_content会怎样呢。试了一下,效果跟match_parent一样,因为自适应没有给他尺寸,所以只能是根据父布局来了。那么如何设置自己想要的的自适应尺寸呢。这里就要重写另一个在自定义view中比较重要的方法onMeasure了。如下

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    //获取宽高的测量模式(如wrap_content为一种模式)
    int widthMode=MeasureSpec.getMode(widthMeasureSpec);
    int heightMode=MeasureSpec.getMode(heightMeasureSpec);

    //获取宽高的大小
    int widthSize=MeasureSpec.getSize(widthMeasureSpec);
    int heightSize=MeasureSpec.getSize(heightMeasureSpec);

    //设置自己用于wrap_conten显示的宽高
    int mwidth=500;
    int mheight=500;
    if(widthMode==MeasureSpec.EXACTLY){
        Log.e("Myview","exactly");
    }
    if(widthMode==MeasureSpec.UNSPECIFIED){
        Log.e("Myview","unspecified");
    }

    /*
    这里的测量模式有三种。
    AT_MOST对应与wrap_content
    EXACTLY对应与match_parent与自己设置尺寸
    UNSPECIFIED未指定尺寸,不常见,在scroview里面控件尺寸受内容影响而不受设定尺寸影响
     */
    if(widthMode==MeasureSpec.AT_MOST){
        widthSize=mwidth;
    }
    if(heightMode==MeasureSpec.AT_MOST){
        heightSize=mheight;
    }
    setMeasuredDimension(widthSize,heightSize);


}

其实上面的view还有一个问题就是padding不生效,因为上述例子在onDraw并没有对padding进行处理,所以肯定不会生效了。要想让padding生效,在onDraw方法中可以用getPaddingLeft()获取左边padding(其他边padding以此类推)然后绘制的时候考虑padding就OK了。
最后说一下自定义属性,就比如用某些别人的控件框架的时候,xml里面有Android:开头的属性(系统自带),app:开头的属性(自定义)。这里说自定义属性该如何去做。首先在values文件夹下创建一个xml文件用于存放自定义属性。
attrs_MycustomView.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--设置一个集合用于放myview的自定义属性,等会根据name解析属性-->
    <declare-styleable name="MyView">
        <!--format表示你自定义属性的格式这里是color(还有很多其他格式如boolean,enum等)。-->
        <!--format的值关乎你xml文件中用它的时候用什么类型来赋值。-->
        <attr name="text_color" format="color"/>
    </declare-styleable>
</resources>

然后在自定义的构造函数中读取它。需要的时候用就行了。

int mColor;
public MyView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);

    //读取刚刚定义的myview属性集合
    TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.MyView);
    //读取属性值。一个参数为资源id(注意格式为:集合名_属性名)。第二个参数为默认属性值.
    mColor=typedArray.getColor(R.styleable.MyView_text_color,Color.BLACK);
    typedArray.recycle();//释放资源
}

我的例子是在之前的view的基础上加上文字。然后文字的颜色由自定义属性确定。在之前的onDraw方法最后追加如下。

Paint paint=new Paint();
paint.setColor(mColor);
paint.setTextSize(60);
canvas.drawText("添加文字",30,getHeight()-30,paint);

xml文件用的时候。

<com.example.mycustomview.MyView
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_gravity="center"
    android:layout_marginTop="50dp"
    app:text_color="#20d217"/>

show4

2. 自定义viewGroup

viewgroup是用来规范其中的子view的,那么自定义viewgroup就是用自己的规则去规范它的子view。
自定义viewgroup需要继承Viewgroup。然后重写两个方法
onMeasure:与自定义view一样完成测量工作,不同的是除了自己的测量还有它的子view的测量。所以必须在onmeasure中调用measureChildren方法设置子view的测量模式或者调用measureChild对单个子view设置测量模式。
onLayout:完成子view的位置确定。关键方法就是子view的layout方法确定位置。
这里给出一个我用自定义viewgroup的方式实现一个仿QQ侧滑(网上一般采用ScrollView)。侧滑效果如下
show5
再给出我的代码及部分注释

public class MyViewGroup extends ViewGroup{

    //屏幕宽高
    private int screenWidth;
    private int screenHeight;
    //滑动起始位置
    int startX=0;

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

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);

        //获取屏幕宽高
        DisplayMetrics displayMetrics=new DisplayMetrics();
        WindowManager manager= (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        manager.getDefaultDisplay().getMetrics(displayMetrics);
        screenWidth=displayMetrics.widthPixels;
        screenHeight=displayMetrics.heightPixels;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //让子控件自己测量自己
        measureChildren(widthMeasureSpec,heightMeasureSpec);

        //固定布局宽高为屏幕宽高
        setMeasuredDimension(screenWidth,screenHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        if(getChildCount()<2) return;
        //只考虑两个布局

        //侧滑界面左边在屏幕外1/3,用于滑动
        View child1=getChildAt(0);
        child1.layout(-child1.getMeasuredWidth()/3,0,child1.getMeasuredWidth()*2/3,child1.getMeasuredHeight());

        View child2=getChildAt(1);
        child2.layout(0,0,child2.getMeasuredWidth(),child2.getMeasuredHeight());
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {

        View child1=getChildAt(0);
        View child2=getChildAt(1);

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                startX=(int)event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                int x=(int)event.getX();
                int distance=x-startX;
                startX=x;
                int lastX=child2.getLeft()+distance;//主界面滑动位置确定
                int lastX2=child1.getLeft()+distance/3;//侧滑界面滑动位置确定
                lastX2=distance>0?lastX2+1:lastX2-1;//距离多个1,为了实现无缝

                //确保位置合法
                if(lastX<0)
                    lastX=0;
                if(lastX>child1.getMeasuredWidth())
                    lastX=child1.getMeasuredWidth();
                if(lastX2<-child1.getMeasuredWidth()/3)
                    lastX2=-child1.getMeasuredWidth()/3;
                if(lastX2>0)
                    lastX2=0;

                //改变位置并重绘
                child1.layout(lastX2,0,lastX2+child1.getMeasuredWidth(),child1.getMeasuredHeight());
                child2.layout(lastX,0,lastX+child2.getMeasuredWidth(),child2.getMeasuredHeight());
                invalidate();

                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }
    
}

调用的时候传人两个布局就行。如下

<com.example.mycustomview.MyViewGroup
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:background="#2dbf30">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="这是侧滑界面"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ed493d">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="这是主界面"/>
    </LinearLayout>
</com.example.mycustomview.MyViewGroup>

这里我实现的这个侧滑跟QQ的比还是有不少缺陷,因为这个实例主要目的是自定义viewgroup的练手,弄清楚自定义viewgroup就行了,没有去把它弄的很完美。这里指出两个最大的缺陷
1.没有弹性和加速度效果。如果想实现这个功能可以借用这两个类VelocityTracker(加速度相关),Scroller(滑动相关)2.
2.没有做margin处理,因为侧滑和主界面都是直接丢LinearLayout也没用到margin所以也没有考虑margin效果。如果考虑margin的话就得修改LayoutParams,具体怎么弄百度上相关文章很多(我很懒!)
3.没有做事件冲突处理。这个东西类似于滑动控件嵌套,如果TextView换成button肯定会出现事件冲突的。解决冲突方法跟一般的情况一样

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值