Android 自定义view<一>

前言:
Android开发与进阶的第一个门槛就是自定义view,大多数的设计不会按照Android所具有的优势和特性去设计,而是一味跟随IOS的设计风格去走,当然IOS的的设计相当漂亮,系统也提供了许多优秀的控件给开发者使用,这也符合苹果的始终所坚持的标准化与统一化。但是要把这些控件所带有的效果同样,甚至功能实现在Android上,是有许多不尽如人意的地方,有许多是Android所不擅长的,还有一些设计是系统无法满足的,所以学习自定view是相当重要的。好多同学一听自定view感觉比较难,想学习网上巴拉巴拉好多基本都是上来就,贴代码,连最基本的自定view的流程都不知道,云里雾里的看一下代码,看不懂,比较难就得过且过了。如果学自定义view最好是结合网上的一些优秀的帖子,和View的源码加官方的doc文档,虽然一些开发的同学英文不是很好还是尽量的去看,译文和原文还是有一定的差距的。

本篇目录结构:

一:查看官方文档及其他资料了解View。

二:整体的了解自定View的具体的流程。

三:对自定View的一些扩展和总结。

<一>查看官方文档及其他资料了解View
首先查看官网了解view,原文就不上了截了一些关键图上来
View的官方说明文档:https://developer.android.com/reference/android/view/View.html
这里写图片描述

View的直接子类和间接子类,Direct Subclasses,是直接的子类的意思,indirect是间接子类的意思,View的间接子类就比较多了。看一下我们自定义View所用的到的几个方法和对应的参数都是做什么的,先看构造方法:
这里写图片描述
、它有四个构造方法,说明一下这几个参数的意思Context这个是最常用的了上下文环境, AttributeSet attrs 是属性集合,int defStyleAttr是默认的指定的一个style中的属性,int defStyleRes默认的一个stye资源。第一个构造方法是在代码当中我们New的时候调用的,第二个构造方法是系统调用的,当布局文件中使用该view,第三个方法一般就是我们自己调用了,至于第四个构造方法,没有深入的研究有两篇讲的比较好的博客给大家,大家可以去深入的了解。
http://blog.csdn.net/yuzhouxiang/article/details/6958017
http://blog.csdn.net/mybeta/article/details/39993449
这两篇讲的比较详细,也比较清楚。

再来看一下onDraw这个方法:

这里写图片描述

提供一个画布供你去操作。

再来看看onMeasure方法:

这里写图片描述

里面返回了2个参数,int widthMeasureSpec, int heightMeasureSpec 父类的约束条件,大概意思就是子类可以重写该方法,但是约定子类重写该方法必须调用 setMeasuredDimension(int, int) 方法存储处理过后的宽和高。
还有一个方法就是onLayout:

这里写图片描述

这个方法在自定义的View当中不常用,但是View还可以作为容器放别的View,这个就比较重要了,他有五个参数是boolean changed, int left, int top,  int right, int bottom,是否发生变化,上下左右这几个参数就是当子View发生变化的时候将会被调用。所以当我们自定义的布局容器里面要摆放子View的位置就在这里面操作。
自定义View大概就了解这些。

<二>整体的了解自定义View的一些流程。

1,根据自己的需求自定义控件属性
2.重写构造方(必须)
3.重写onDraw方法(必须)
4.重写onMeasure
5.重写onLayout**

流程1,4,5根据自己的View去分析要不要重写,但是2,3必须要重写。

我们自定义一个View,类似于Textview的一个控件吧,先来自定义属性,首先是字体大小,字体颜色,字体text这几个就行了。
1,.自定义属性:在values下的新建attrs.xml文件,里面自定义的属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--定义属性-->
    <attr name="mText" format="string"></attr>
    <attr name="mTextColor" format="color"></attr>
    <attr name="mTextSize" format="dimension"></attr>
    <!--指定具体的View所拥有的属性-->
    <declare-styleable name="MyTextView">
        <attr name="mText"></attr>
        <attr name="mTextColor"></attr>
        <attr name="mTextSize"></attr>
    </declare-styleable>
</resources

format指定了参数的类型,类型共有这几种:

这里写图片描述

declare-styleable指定了那个View拥有那几个属性。

在XML布局文件中使用需要:

<?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"
    xmlns:customview="http://schemas.android.com/apk/res-auto"
  >
    <com.example.customviewdemo.customview.MyTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        customview:mText="测试文本啊哈哈哈哈哈哈哈,哈哈哈啊哈哈哈哈大街上来看建档,laaldlsakdl,立卡简单快乐数据库第六届奥斯卡拉家带口老实交代啦"
        customview:mTextSize="15sp"
        customview:mTextColor="#2a2a58"
        android:padding="20dp"
        android:background="#ff0000"
        >
    </com.example.customviewdemo.customview.MyTextView>
</LinearLayout>

xmlns:customview=”http://schemas.android.com/apk/res-auto”,里面的customview可以自己定义,在控件中引用是必须使用该名字。
这样简单的自定属性就好了,还要把自定义的属性的值取出来用。

2.重写构造方法

   public MyTextView(Context context) {
        this(context, null);
    }
    public MyTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取指定View所拥有的属性的列表
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextView, defStyleAttr, 0);
        text = typedArray.getString(R.styleable.MyTextView_mText);
        textColor = typedArray.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK);
        textSize = typedArray.getDimensionPixelSize(R.styleable.MyTextView_mTextSize, 100);
        typedArray.recycle();
        initData();
    }

我是让第一个构造方法调第二个构造方法,第二个调用第三个构造方法,在第三个方法里面把布局里面的属性的值取出来,当解析布局文件后,将属性集合传回来,我们根据attrs中定义的属性id来获取对应的值,取完一定要回收typedArray.recycle();,不然会报错。取到值就该干嘛,干嘛。

3.重写onDraw将text,color,textsize设置
在重写之前先把设置一下字体的颜色,字体的大小与颜色。

 //初始化数据
    private void initData() {
        textList = new ArrayList<>();
        paint = new Paint();
        rect = new Rect();
        //获取
        paint.setTextSize(textSize);
        paint.setColor(textColor);
        //将字符串的填充到矩形中
        paint.getTextBounds(text, 0, text.length(), rect);
    }

我们直接无法获取字符串的宽度和高度,别说是length那是字符串的长度。利用Rect,将字符串填充到一个长方形中,获取该长方形的宽高就是字符传的宽高。这是重写的onDraw方法:

 //一般重写ondraew方法,
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas); 
 //String text, float x, float y, Paint paint
//1.第一个参数是要写的字符串,第二个参数是字符串开始的X轴上的位置,第三个参数是Y轴的坐标,第三个参数是画笔
//2.计算字符串要显示到画布上的位置,如果要居中显示,控件大小的一半减去字符串一半的位置作为起始点即X轴坐标,高度也是一样的
//3.将设置了颜色的画笔设置好
//canvas.drawText(text, getWidth() / 2 - rect.width() / 2, getHeight() / 2 -rect.height() / 2, paint);
//换行,满一行换一行,动态的设置起始点的Y轴的位置,
 for (int i = 0; i < textList.size(); i++) {
 paint.getTextBounds(textList.get(i), 0, textList.get(i).length(), rect);
 //speacd是行间距了,不然每一行与下面的一行靠的比较近,比较难看,第一行就不需要了,第一行有padingtop
 canvas.drawText(textList.get(i), (getWidth() / 2 - rect.width() / 2), (getPaddingTop() +sepcpading*i+ rect.height() * i), paint);
   }
 }

有的同学可能一看里面的计算就蒙蔽了不知道算啥呢,画个图就知道了

这里写图片描述

T就是top的高度,H就是文本的高度,S就是间隔的高度,所以每一行开始的Y轴坐标是
(getPaddingTop() +sepcpading*i+ rect.height() * i)
至于宽度我们是让文本居中显示的看图:

这里写图片描述

黄色的是控件,红色的是字符串 他们的一半之差就是在控价显示,在X轴上的坐标。因此是
getWidth() / 2 - rect.width() / 2
到这里控件就可以显示文本内容了,但是当我们设置的文本内容比较长的时候会抛出控件之外看不见,还有一个问题就是让我们设置了控件的宽高是包裹着内容,的时候控件会充满父控件,老司机估计都遇到过这个问题。
先解决控件宽度设置为
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
后充满父控件,的原因。先看一哈默认的onMeasure的源码:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

根据specMode 给返回一个值,当这个specMode返回类型是MeasureSpec.UNSPECIFIED返回控件所支持的最小值,当specMode的是 case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY:返回的数据是父控件给予的最大空间,所以再看看MeasureSpec,是定义在View中的一个内部类:

/**
 * measurespec封装了父控件对他的孩子的布局要求。
 * 一个measurespec由大小和模式。有三种可能的模式:
 */
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;  //0


    public static final int EXACTLY     = 1 << MODE_SHIFT;  //1073741824


    public static final int AT_MOST     = 2 << MODE_SHIFT;   //-2147483648

    public static int makeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

MeasureSpec 有三种模式,UNSPECIFIED ,EXACTLY ,AT_MOST 三种状态,未指定,精确的,至多。在什么情况下会给这三种状态:看下面的图:

这里写图片描述

所以当我们设置例 了wrap_content返回的是AT_MOST 所以会全屏展示。

第二个问题是超出控件,解决方法是换行,当我们把获取到的字符串,根据字符串的宽度去截取字符串,计算出行数,显示出来就可以了。
要想解决这两个问题就必须重写onMeasure去根据控件的实际尺寸和父控件所给的约数条件去切割字符串,计算行数,存储宽高。

//当View的宽高设置为wrap_content时,父控件的宽高为match_parent,子控件将会全屏显示将会全屏展示
    //MeasureSpec.AT_MOST和MeasureSpec.EXACTLY返回的都是父控件所剩余的空间,
    // 在子控件为wrap_content和match_parent下都会返回父控件的剩余空间造成上面的现象出现
    //解决方法:重写测量方法当模式为MeasureSpec.AT_MOST时将子空间的大小返回给父类即可。
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取宽高模式,模式为数字
        int widthModle = MeasureSpec.getMode(widthMeasureSpec);
        int heightModle = MeasureSpec.getMode(heightMeasureSpec);

        //获取父类的约数条件下的宽高
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        Log.v("custom", "宽的模式:"+widthModle);
        Log.v("custom", "高的模式:"+heightModle);
        Log.v("custom", "宽的尺寸:"+widthSize);
        Log.v("custom", "高的尺寸:"+heightSize);

        int textWidth = rect.width();

        Log.v("custom", "文所需宽度:"+textWidth);
        if (textList.size() == 0) {
            //没有将总的字符串分割,判断一行最多能够放多长的字符串,即父类所给的剩余空间的宽度减去控件内部
           // 的pading即为一行的最大长度。
            int pading = getPaddingLeft() + getPaddingRight();
            int speaceWidth = widthSize - pading;
            Log.v("custom", "除去pading的的宽度:"+speaceWidth);
            //字符串的长度小于减去pading的长度,一行可以放得下
            if (textWidth < speaceWidth) {
                textList.add(text);
                lineNum = 1;
            } else {
                isOneLines = false;
                spLineNum = textWidth / speaceWidth;
                Log.v("custom", "没有去除小数的行数:"+spLineNum);
                //除不清,说明多一行需额外加1行
                if ((spLineNum + "").contains(".")) {
                    lineNum = Integer.parseInt((spLineNum + "").substring(0, (spLineNum + "").indexOf("."))) + 1;
                    Log.v("custom", "去除小数之后的行数"+lineNum);
                } else {
                    lineNum = spLineNum;
                }
                //字符串的总长度/行数,即每一行的字符串的长度
                float nLine = (text.length() / lineNum);
                int lineLength;
                if((nLine + "").contains(".")){
                    lineLength = Integer.parseInt((nLine + "").substring(0, (nLine + "").indexOf("."))) + 1;
                }else {
                    lineLength=(int)nLine;
                }
                Log.v("custom", "文本内容:"+text);
                Log.v("custom", "文本总长度:"+text.length());
                Log.v("custom", "能绘制文本每一行的字符串的长度:"+lineLength);
                Log.v("custom", "需要绘制:"+lineNum+"行");
                Log.v("custom", "lineLength:"+lineLength);
                //分割字符串,将字符串分割成
                for (int i = 0; i < lineNum; i++) {
                    String lineStr;
                    if (text.length() < lineLength) {
                        lineStr = text.substring(0, text.length());
                    } else {
                        lineStr = text.substring(0, lineLength);
                    }
                    textList.add(lineStr);
                    //每次重新给text辅助覆盖,使得text的值越来越少,直到空为止
                    if (!TextUtils.isEmpty(text)) {
                        if (text.length() < lineLength) {
                            text = text.substring(0, text.length());
                        } else {
                            text = text.substring(lineLength, text.length());
                        }
                    } else {
                        break;
                    }
                }
            }
        }
        int width;
        int height;
        if (widthModle == MeasureSpec.EXACTLY) {
            //如果是精确数据直接保存父类给予的精确空间,否则就返回字符串的宽度
            width = widthSize;
        } else {
            if (isOneLines) {
                //padding+文本的宽度就是整个子控件的宽度
                width = getPaddingLeft() + textWidth + getPaddingRight();
            } else {
                //多行处理,返回父控件的宽度,即
                width = widthSize;
            }
        }
        if (heightModle == MeasureSpec.EXACTLY) {
            //如果是精确数据直接保存父类给予的精确空间,否则就返回字符串的宽度
            height = heightSize;
        } else {
            float textHeight = rect.height();
            if(isOneLines){
                height = (int)(getPaddingTop() + textHeight + getPaddingBottom());
            }else {
                //多行根据行高设置
                height=(int)(getPaddingTop() + textHeight*lineNum + getPaddingBottom());
            }

        }
        setMeasuredDimension(width, height);
    }

代码量不大,只是处理一些细节,首先是判断字符串是否用一行就可以展示,如果不够就去计算需要几行,平均每行要几个字符串,计算出肌肤穿的个数,从总出的字符串里面截取出来放在list中,最后有?Ondraw去绘制出来即可。这里要注意一下,当计算到行数与每一行字符串的个数时有可能产生小数,当计算行有小数是说明除不清,还需要一行,需要多加一行;计算每一行所需字符串时,有小数说明比如字符串总长度56,有四行。57/4=14.25,int默认是14,将会导致部分字符串取不到丢失,判断一下自动多加一个字符就可以了。

最后的保存的控件高度就是top+行数X行高+(行数-1)*X行距+bottom就可以啦,宽度就是leftpading+行宽+rightPading就可以啦。

总结:
1.自定义属性
2.重写构造方法
3.重写onDraw
4.重写onMeasure
知道了这些知识,对于该控件的还有许多可以扩展的:
简单的扩展有文本显示方向,靠左靠右,居中,控件底部显示,甚至竖直方向的显示都是可以实现的。只要你理解了,就可以举一反三的操作,展示。
当然这个只是简单的自定义view,学会这些还不足以满足开发。后面我将会边学边写,将自定义一些比较复杂的view来不断地提高自己。

结果图:

这里写图片描述

GitHub:https://github.com/AndrewGeorge/customviewdemo

参考资料:
https://developer.android.com/guide/topics/graphics/2d-graphics.html
http://blog.csdn.net/xmxkf/article/details/51454685
http://blog.csdn.net/yuzhouxiang/article/details/6958017
http://blog.csdn.net/mybeta/article/details/39993449

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值