控件是android开发过程中必不可少的部分,官方本身也给我们提供了许多的控件以供我们使用。但是有的时候已有控件并不能满足我们的需求,所以这个时候就需要我们自己来自定义控件。如果我们要学习自定义控件的话,我们需要去学习view的绘制过程这方面的知识,如果你需要为你的控件加上动画的话,你还需要学习android动画方面的知识,在这个过程中你可能还要学习一下android分发机制。有了这些基本的知识后,你就可以来自定义控件了。
下面我们就来实现一个简单的自定义控件CircleList,效果如下
上面控件主要的功能就是点击中间按钮,弹出几个子按钮,再次点击隐藏子按钮,在弹出和隐藏的过程中为子按钮添加旋转动画。
那么现在我们就来实现这个控件。
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>