Android自定义控件与动画的使用

控件是android开发过程中必不可少的部分,官方本身也给我们提供了许多的控件以供我们使用。但是有的时候已有控件并不能满足我们的需求,所以这个时候就需要我们自己来自定义控件。如果我们要学习自定义控件的话,我们需要去学习view的绘制过程这方面的知识,如果你需要为你的控件加上动画的话,你还需要学习android动画方面的知识,在这个过程中你可能还要学习一下android分发机制。有了这些基本的知识后,你就可以来自定义控件了。
下面我们就来实现一个简单的自定义控件CircleList,效果如下
Android自定义控件与动画的使用_第1张图片

上面控件主要的功能就是点击中间按钮,弹出几个子按钮,再次点击隐藏子按钮,在弹出和隐藏的过程中为子按钮添加旋转动画。
那么现在我们就来实现这个控件。

1.首先分析一下这个控件,它由数个CircleIamgeView组成(CircleImageView控件在网上可以下载,在这里不分析它的实现方式),其实它就好像是一个viewgroup,包含了数个circleImageView子控件,所以这个控件它就继承于viewgroup。

public class CircleList extends ViewGroup
{
    //实现它的多个构造函数
    public CircleList(Context context)
    {
        super(context);
    }

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

    public CircleList(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
    }

    public CircleList(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
    {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
    //重写它的layout方法
    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3)
    {

    }
}

上面是继承viewgroup后需要我们重写的一些方法,可以看到,除了四个构造函数外,还有onLayout这个方法需要我们来实现。

如果你了解view的绘制过程你应该知道,第一步我们需要对这个控件进行测量,也就是Measure.。

1.Measure
在自定义控件的时候,我们需要重写onMeasure方法来对控件进行一个测量。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //从参数中得到高度和宽度的测量规格
        //获取宽度和高度的测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //获取宽度和高度的大小
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width = widthSize;
        int height = heightSize;
        Log.e("init","ccc");
        switch (widthMode)
        {
            case MeasureSpec.UNSPECIFIED:
                width = widthSize;
                break;
            case MeasureSpec.EXACTLY:
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST:
                //设置一个合适的值作为默认大小
                width = mSize;
                break;
        }
        switch (heightMode)
        {
            case MeasureSpec.UNSPECIFIED:
                height = heightSize;
                break;
            case MeasureSpec.EXACTLY:
                height = heightSize;
                break;
            case MeasureSpec.AT_MOST:
                height = mSize;
                break;
        }
        mWidth=width;
        mHeight=height;
        //设置测量后该控件的大小
        setMeasuredDimension(width,height);
        Log.e("draw",width+","+height+"");
        //因为我们需要一个正方形的布局,所以对高度和宽度进行判断,选择小的作为布局的依据
        MinSize = width > height ? height : width;
        this.pading = MinSize/10;
        //测量子view
        measureChildren(width,height);
    }

由上面代码可以看的,onMeasure这个方法中有widthMeasureSpec,heightMeasureSpec这两个参数,这两个int型参数就是你用来测量当前view的依据。
MeasureSpec参数是一个32位的Int类型数据。最高的两位代表了SpecMode(测量模式),后面的低30位代表了SpecSize(规格大小)。SpecMode有三个值,分别是UNSPECIFIED、EXACTLY、AT_MOST。
UNSPECIFIED:不指定测量模式,子视图可以是任意尺寸。
EXACTLY:精确测量模式。当设置具体数值或match_parent时生效。
AT_MOST:最大值模式,当设置为wrap_content时生效。

所以我们通过MeasureSpec.getMode与MeasureSpec.getSize这两个方法将宽与高的测量模式和规格大小提取出来。对测量模式进行一个判断。如果是AT_MOST(也就是设置了wrap_content)模式的话,则设置一个合适的大小,防止控件不显示。如果为其他两种模式的话就将得到的规格大小作为最终测量的结果。调用setMeasuredDimension(width,height)方法设置好最终了控件大小。因为当前控件继承的是viewgroup所以我们还需要将当前view测量好的规格大小,通过 measureChildren(width,height)传递个给子view,让子view也进行一个测量。

在子view测量之前,你可能会有一个疑问,就是我们并没有给这个viewGroup添加子view啊,其实我们在构造函数中需要进行一个初始化,为当前控件添加它的子view。

private void init(Context context,AttributeSet attr)
    {
        //得到xml中设置的自定义的属性(具体用法,下面会讲)
        TypedArray typedArray = context.obtainStyledAttributes(attr,R.styleable.CircleList);
        //按钮的总个数
        this.mNum = typedArray.getInt(R.styleable.CircleList_button_num,5);
        //是否展开四周的的按钮
        this.Isshow = typedArray.getBoolean(R.styleable.CircleList_is_show,false);
        //初始化所有的按钮
        this.mButtons = new CircleImageView[mNum];
        //初始化坐标点集合,分别代表按钮当前的位置坐标、按钮隐藏时所在坐标、按钮显示时所在坐标。
        this.mPoints = new Point[mNum];
        this.mHidePoints = new Point[mNum];
        this.mShowPoints = new Point[mNum];
        //初始化一个Image数组,用于给按钮添加背景,如果不自己设置背景的话则用下面的Image作为背景,也可以自己设置
        int[] buttonImage=new int[]{R.drawable.img1,R.drawable.img2,R.drawable.img3,R.drawable.img4,R.drawable.img5,R.drawable.img6,R.drawable.img7
                ,R.drawable.img8,R.drawable.img9};
        this.mButtonImage = buttonImage;
        //为每一个按钮设置默认背景,并使用addView把子view加入到当前biewgroup
        for(int i=(mNum-1);i>=0;i--)
        {
            Log.e("init","ddd");
            mButtons[i] = new CircleImageView(context);
                 mButtons[i].setImageBitmap(BitmapFactory.decodeResource(context.getResources(),mButtonImage[i]));
            //为每一个按钮添加Cilck时间监听
            mButtons[i].setOnClickListener(this);
            //给每一个按钮设置id,便于区别
            mButtons[i].setId(i);
            //将按钮加入到当前viewgroup
            addView(mButtons[i],mNum-i-1);
        }
    }

在init()方法中,我们主要是为我们需要用到的一些变量做赋值,包括指定个数按钮的初始化,三个point数组的的初始化等。

接着们需要来对每一个子view(也就是按钮)进行测量。

@Override
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        Log.e("init","eee");
        //循环为每一个子view进行测量
        for(int i=(mNum-1);i>=0;i--) {
            //申明高和宽的MeasureSpec
            int widthMS;
            int heightMS;
            //通过按钮的数量来得到每一个按钮间应该间隔的角度
            double scale = Math.PI / (mNum - 2)/2;
            //设置四周按钮与中间按钮的距离
            double radio = MinSize / 3;
            Log.e("draw","min"+MinSize);
            //初始化隐藏时按钮所在的坐标
            mHidePoints[i]=new Point(MinSize/4,MinSize/4);
            if (i == 0) {
                //设置中间按钮的半径
                mRadio = MinSize / 4 - pading;
                //设置中间按钮显示时所在位置
                mShowPoints[i] = new Point(MinSize / 4, MinSize / 4);
                Log.e("draw",i+":radio:"+mRadio);
                //设置MeasureSpec
                widthMS = MeasureSpec.makeMeasureSpec(mRadio*2, MeasureSpec.EXACTLY);
                heightMS = MeasureSpec.makeMeasureSpec(mRadio*2, MeasureSpec.EXACTLY);
            } else {
                //
                mRadio_ = mRadio / 3*2;
                Log.e("draw",i+":radio:"+mRadio_+"dd"+(Math.cos((i - 1) * scale)));
                mShowPoints[i] = new Point((int) (Math.cos((i - 1) * scale) * (radio+MinSize/4)+mRadio_), (int) (Math.sin((i - 1) * scale) * (radio+MinSize/4)+mRadio_));
                widthMS = MeasureSpec.makeMeasureSpec(mRadio_ * 2, MeasureSpec.EXACTLY);
                heightMS = MeasureSpec.makeMeasureSpec(mRadio_ * 2, MeasureSpec.EXACTLY);
            }
            mPoints = Isshow ? mShowPoints : mHidePoints;
            measureChild(mButtons[i],widthMS,heightMS);
        }

因为保存失败,又导入新的文章将原来的全部冲掉了。导致后面写的三分之二全部不见了,有一种想死的感觉,csdn在某些使用方面真的有些操蛋,现在没有心情重新了写了,这里就把所有源码贴出来,加上注释。以后有心情了再重新修改。

package com.example.admin.mytest;

import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.renderscript.Sampler;
import android.support.annotation.ColorInt;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.widget.Button;
import android.widget.TextView;

/**
 * Created by wade on 2017/12/31.
 */

public class CircleList extends ViewGroup implements View.OnClickListener{

    //是否展开
    private boolean Isshow = false;
    //button数量
    private int mNum;
    //button对应的背景
    private int[] mButtonImage;
    //button数组
    private CircleImageView[] mButtons;
    //画笔
    private Paint mPaint;
    //控件宽,高度
    private int mWidth;
    private int mHeight;
    //中间圆的半径
    private int mRadio;
    //四周圆的半径
    private int mRadio_;
    //设置为wrap_content时的大小
    private int mSize = 400;
    //按钮实时的位置
    private Point[] mPoints;
    //按钮收缩时的位置
    private Point[] mHidePoints;
    //按钮展开时位置
    private Point[] mShowPoints;
    private int MinSize;
    //中间按钮边距
    private int pading = 40;
    //用于按钮点击的对象
    private onItemClickListener onItemClickListener =null;
    //设置属性动画
    ValueAnimator mShowObjectAnimator;
    ValueAnimator mHideObjectAnimator;
    //接口
    public static interface onItemClickListener{
        void onClick(View v, int position);
    }
    //构造函数初始化对象
    public CircleList(Context context) {
        super(context);
        init(context,null);
    }

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

    public CircleList(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context,attrs);
    }

    public CircleList(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context,attrs);
    }


    //布局
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //循环为每一个按钮布局,根据点的位置和按钮的半径,调用子view自身的layout进行布局
        for(int i=0;iif(i==0){
                //中间的按钮
                mButtons[i].layout(mPoints[i].x-mRadio,mPoints[i].y-mRadio,mRadio*2+mPoints[i].x-mRadio,mRadio*2+mPoints[i].y-mRadio);
            }else {
                //四周的按钮
                mButtons[i].layout(mPoints[i].x-mRadio_,mPoints[i].y-mRadio_,mRadio_*2+mPoints[i].x-mRadio_,mRadio_*2+mPoints[i].y-mRadio_);
            }
            Log.e("init","ggg");
        }
    }
    //测量
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //从参数中得到高度和宽度的测量规格
        //获取宽度和高度的测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //获取宽度和高度的大小
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width = widthSize;
        int height = heightSize;
        Log.e("init","ccc");
        switch (widthMode)
        {
            case MeasureSpec.UNSPECIFIED:
                width = widthSize;
                break;
            case MeasureSpec.EXACTLY:
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST:
                //设置一个合适的值作为默认大小
                width = mSize;
                break;
        }
        switch (heightMode)
        {
            case MeasureSpec.UNSPECIFIED:
                height = heightSize;
                break;
            case MeasureSpec.EXACTLY:
                height = heightSize;
                break;
            case MeasureSpec.AT_MOST:
                height = mSize;
                break;
        }
        mWidth=width;
        mHeight=height;
        //设置测量后该控件的大小
        setMeasuredDimension(width,height);
        Log.e("draw",width+","+height+"");
        //因为我们需要一个正方形的布局,所以对高度和宽度进行判断,选择小的作为布局的依据
        MinSize = width > height ? height : width;
        this.pading = MinSize/10;
        //测量子view
        measureChildren(width,height);
    }

    @Override
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        Log.e("init","eee");
        for(int i=(mNum-1);i>=0;i--) {
            int widthMS;
            int heightMS;
            double scale = Math.PI / (mNum - 2)/2;
            double radio = MinSize / 3;
            Log.e("draw","min"+MinSize);
            mHidePoints[i]=new Point(MinSize/4,MinSize/4);
            if (i == 0) {
                mRadio = MinSize / 4 - pading;
                mShowPoints[i] = new Point(MinSize / 4, MinSize / 4);
                Log.e("draw",i+":radio:"+mRadio);
                widthMS = MeasureSpec.makeMeasureSpec(mRadio*2, MeasureSpec.EXACTLY);
                heightMS = MeasureSpec.makeMeasureSpec(mRadio*2, MeasureSpec.EXACTLY);
            } else {
                mRadio_ = mRadio / 3*2;
                Log.e("draw",i+":radio:"+mRadio_+"dd"+(Math.cos((i - 1) * scale)));
                mShowPoints[i] = new Point((int) (Math.cos((i - 1) * scale) * (radio+MinSize/4)+mRadio_), (int) (Math.sin((i - 1) * scale) * (radio+MinSize/4)+mRadio_));
                widthMS = MeasureSpec.makeMeasureSpec(mRadio_ * 2, MeasureSpec.EXACTLY);
                heightMS = MeasureSpec.makeMeasureSpec(mRadio_ * 2, MeasureSpec.EXACTLY);
            }
            mPoints = Isshow ? mShowPoints : mHidePoints;
            measureChild(mButtons[i],widthMS,heightMS);
        }
    }

    @Override
    protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
        child.measure(parentWidthMeasureSpec,parentHeightMeasureSpec);
    }

    private void init(Context context,AttributeSet attr)
    {
        TypedArray typedArray = context.obtainStyledAttributes(attr,R.styleable.CircleList);
        this.mNum = typedArray.getInt(R.styleable.CircleList_button_num,5);
        this.Isshow = typedArray.getBoolean(R.styleable.CircleList_is_show,false);
        this.mButtons = new CircleImageView[mNum];
        this.mPoints = new Point[mNum];
        this.mHidePoints = new Point[mNum];
        this.mShowPoints = new Point[mNum];
        int[] buttonImage=new int[]{R.drawable.img1,R.drawable.img2,R.drawable.img3,R.drawable.img4,R.drawable.img5,R.drawable.img6,R.drawable.img7
                ,R.drawable.img8,R.drawable.img9};
        this.mButtonImage = buttonImage;
        for(int i=(mNum-1);i>=0;i--)
        {
            Log.e("init","ddd");
            mButtons[i] = new CircleImageView(context);
            //mButtons[i].setBackgroundResource(mButtonImage[i]);
            mButtons[i].setImageBitmap(BitmapFactory.decodeResource(context.getResources(),mButtonImage[i]));
            mButtons[i].setOnClickListener(this);
            mButtons[i].setId(i);
            addView(mButtons[i],mNum-i-1);
        }
    }
    public void setmButtonImage(int[] mButtonImage) {
        this.mButtonImage = mButtonImage;
        for(int i=0;i@Override
    public void draw(Canvas canvas) {
        Log.e("init","qqq");
        super.draw(canvas);
    }

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


    @Override
    public void onClick(View v) {
        if(v.getId()==0){
            if(Isshow){
                HideObjectAnimator();
                Isshow=false;
            }else {
                ShowObjectAnimator();
                Isshow=true;
            }
            return;
        }
        if(onItemClickListener!=null){
            onItemClickListener.onClick(v,v.getId());
        }
    }

    public void setOnItemClickListener(onItemClickListener monItemClickListener){
        onItemClickListener = monItemClickListener;
    }

    public void ShowObjectAnimator(){
        if(mShowObjectAnimator==null){
            PointsChange hidePointChange = new PointsChange(mHidePoints);
            PointsChange showPointChange = new PointsChange(mShowPoints);
            mShowObjectAnimator = ValueAnimator.ofObject(new PointsEvaluator(),hidePointChange,showPointChange);
        }
        mShowObjectAnimator.setDuration(500);
        mShowObjectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointsChange ingPC = (PointsChange) animation.getAnimatedValue();
                mPoints = ingPC.getmPoints_();
                //Log.e("point",ingPC.getPoint(2).x+":"+ingPC.getPoint(2).y);
                onLayout(true,0,0,0,0);
            }
        });
        SetAnimator(mShowObjectAnimator,false);
        //mShowObjectAnimator.start();
    }

    public void HideObjectAnimator(){
        if(mHideObjectAnimator==null){
            PointsChange hidePointChange = new PointsChange(mHidePoints);
            PointsChange showPointChange = new PointsChange(mShowPoints);
            mHideObjectAnimator = ValueAnimator.ofObject(new PointsEvaluator(),showPointChange,hidePointChange);
        }
        mHideObjectAnimator.setDuration(500);
        mHideObjectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointsChange ingPC = (PointsChange) animation.getAnimatedValue();
                mPoints = ingPC.getmPoints_();
                //Log.e("point",ingPC.getPoint(2).x+":"+ingPC.getPoint(2).y);
                onLayout(true,0,0,0,0);
            }
        });
        SetAnimator(mHideObjectAnimator,true);
        //mHideObjectAnimator.start();
    }

    public void SetAnimator(ValueAnimator va,boolean isShow){
        ObjectAnimator[] mOA=new ObjectAnimator[mNum];
        ObjectAnimator[] mOA1=new ObjectAnimator[mNum];
        AnimatorSet animSet = new AnimatorSet();
        AnimatorSet.Builder mm =animSet.play(va);
        for(int i=1;i"rotation", 0f, 10800f).setDuration(300);
            mOA1[i]=ObjectAnimator.ofFloat(mButtons[i], "rotation", 0f, 180000f).setDuration(600);
            if(isShow){
                mm=mm.with(mOA1[i]).after(mOA[i]);
            }else {
                mm=mm.with(mOA1[i]).before(mOA[i]);
            }
        }
        //animSet.setDuration(700);
        animSet.start();
    }


    public class PointsChange{
        private Point[] mPoints_;
        public PointsChange(Point[] points){
            mPoints_ = points;
        }
        public Point getPoint(int i){
            return mPoints_[i];
        }
        public Point[] getmPoints_(){
            return mPoints_;
        }
        public void setPoint(int i,Point point){
            mPoints_[i]=point;
        }

    }
    public class PointsEvaluator implements TypeEvaluator{

        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {
            PointsChange startPC = (PointsChange)startValue;
            PointsChange endPC = (PointsChange)endValue;
            Point[] points =new Point[mNum];
            PointsChange ingPC =new PointsChange(points);
            Point point0 =new Point(endPC.getPoint(0).x,endPC.getPoint(0).y);
            ingPC.setPoint(0,point0);
            for(int i=1;iif(Isshow){
                    fraction = fraction+(i-1)*(i-1)*(i-1)*0.008f;
                }else {
                    fraction = fraction+((mNum-i-1)*(mNum-i-1)*(mNum-i-1)*0.008f);
                }
                if(fraction>1){
                    fraction =1.0f;
                }
                Point point =new Point((int)((endPC.getPoint(i).x-startPC.getPoint(i).x)*fraction+startPC.getPoint(i).x),(int)((endPC.getPoint(i).y-startPC.getPoint(i).y)*fraction+startPC.getPoint(i).y));
                //Log.e("point11",endPC.getPoint(i).x-startPC.getPoint(i).x+":"+point.x+":"+point.y);
                ingPC.setPoint(i,point);
            }
            return ingPC;
        }
    }
}

<resources>
    <declare-styleable name="CircleImageView">
        <attr name="border_width" format="dimension" />
        <attr name="border_color" format="color" />
        <attr name="border_overlay" format="boolean" />
    declare-styleable>
    <declare-styleable name="CircleList">
        <attr name="button_num" format="integer"/>
        <attr name="is_show" format="boolean"/>
    declare-styleable>
resources>

你可能感兴趣的:(Android)