美文网首页Android自定义View
Android 之 自定义控件 之 View

Android 之 自定义控件 之 View

作者: 帅气的欧巴 | 来源:发表于2016-08-24 16:12 被阅读292次

    介绍:

    Android的framework有大量的Views用来与用户进行交互并显示不同种类的数据,但是在实际开发中经常会遇到现有的UI控件不能满足项目需求,或一个功能涉及到多个UI控件的组合,或实现某一特效的UI,这时必须通过自定义View的方式,实现这些功能。

    方式:

    1.多个控件组合在一起
    2.继承至目前已经提供的某个基础的View
    3.继承至View或ViewGroup

    View 和 ViewGroup的关系

    View是基类,一般表示具体到某个控件,ViewGroup是它的子类,但又是layout的基类,一般作为一个视图容器。由于ViewGroup是View的子类,所以View的方法基本都有,但是View的直接子类不具备ViewGroup的属性。虽然他们是继承关系,但是一般情况下我们可以看作是两种形式,一种是控件,一种是容器。

    View的三大核心方法onMeasure、onLayout、onDraw

    onMeasure:用于测量视图的大小;
    onLayout:用于给视图进行布局;
    onDraw:用于对视图进行绘制;

    View重绘方法

    invalidate():
    当view的某些内容发生变化的时候,需要调用invalidate来通知系统对这个view进行重绘。
    requestLayout():
    当某些元素变化会引起组件大小变化时,需要调用requestLayout方法。

    开始

    1.继承至View
    为了让Android Developer Tools能够识别你的view,你必须至少提供一个构造方法,它包含一个Contenx与一个AttributeSet对象作为参数。这个狗杂哦方法允许布局编辑器创建并编辑你的view的实例。

    public class MyView extends View {
        public MyView(Context context) {
            super(context);
        }
        public MyView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    }
    

    2.自定义属性
    为了添加一个内置的View到你的UI上,你需要通过XML属性来指定它的样式与行为。良好的自定义views可以通过XML添加来改变样式,为了让你的自定义的view也有如此的行为,你应该:

    为你的view在资源标签下定义自设的属性
    在你的XML layout中指定属性值
    在运行时获取属性值
    把获取到的属性值应用在你的view上

    添加 资源文件到你的项目中。放置于res/values/attrs.xml文件中。

     <!--
        reference 参考某一资源ID
        dimension 尺寸值
        boolean:布尔值
        integer 整数型
        string  字符型
        color 颜色值
        ...等等
    
       名称对应你所建立的View类名
        -->
        <declare-styleable name="MyView">
            <attr name="titleName" format="string|reference"/>
            <attr name="icon" format="reference"/>
            <attr name="titleColor" format="color"/>
            <attr name="count" format="integer"/>
            <attr name="titleSize" format="dimension"/>
            <attr name="typeface">
                  <enum name="normal" value="0" />
                  <enum name="sans" value="1" />
                  <enum name="serif" value="2" />
                  <enum name="monospace" value="3" />
             </attr>
        </declare-styleable>
        
    

    一旦你定义了自设的属性,你可以在layout XML文件中使用它们,就像内置属性一样。唯一不同的是你自设的属性是归属于不同的命名空间。不是属于http://schemas.android.com/apk/res/android 的命名空间,它们归属于http://schemas.android.com/apk/res/你的包名
    或者 使用http://schemas.android.com/apk/res-auto

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">
     <com.example.customviews.MyView
         custom:titleName="hello world"
         custom:titleColor="#333333"
         custom:typeface="normal" />
    </LinearLayout>
    

    当view从XML layout被创建的时候,在xml标签下的属性值都是从resource下读取出来并传递到view的constructor作为一个AttributeSet参数。尽管可以从AttributeSet中直接读取数值,可是这样做有些弊端:

    拥有属性的资源并没有经过解析
    Styles并没有运用上

    所以我们需要通过obtainStyledAttributes()来获取属性值。这个方法会传递一个TypedArray对象。

    //TypedArray是一个用来存放由context.obtainStyledAttributes获得的属性的数组
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
     try {
           CharSequence titlleName = a.getString(R.styleable.MyView_titleName, false);
           int count = a. getInteger(R.styleable.MyView_count, 0);
           int icon=a.getResourceId(R.styleable.MyView_icon, 0);
       } finally {
           a.recycle();
       }
    

    TypedArray使用完成后一定要调用其recycle方法,否则会有内存泄露的问题

    3.onDraw绘制
    重绘一个自定义的view的最重要的步骤是重写onDraw()方法。onDraw()的参数是一个Canvas对象。Canvas类定义了绘制文本,线条,图像与许多其他图形的方法。你可以在onDraw方法里面使用那些方法来创建你的UI。在你调用任何绘制方法之前,你需要创建一个Paint对象。

    android.graphics framework把绘制定义为下面两类:
    绘制什么,由Canvas处理
    如何绘制,由Paint处理

    常见的操作:
    绘制文字使用drawText()。指定字体通过调用setTypeface(), 通过setColor()来设置文字颜色.
    绘制基本图形使用drawRect(), drawOval(), drawArc(). 通过setStyle()来指定形状是否需要filled, outlined.
    绘制一些复杂的图形,使用Path类. 通过给Path对象添加直线与曲线, 然后使用drawPath()来绘制图形. 和基本图形一样,paths也可以通过setStyle来设置是outlined, filled, both.
    通过创建LinearGradient对象来定义渐变。调用setShader()来使用LinearGradient。
    通过使用drawBitmap来绘制图片.
    例:

    /**
         * 
         * @param canvas
         */
        @Override
        protected void onDraw(Canvas canvas) {
            //drawRect(RectF rect, Paint paint) //绘制区域,参数一为RectF一个区域
            RectF rect = new RectF(10,100,300,300);
            mPaint.setStyle(Paint.Style.FILL);  //填充
            canvas.drawRect(rect,mPaint);
    
    
            //drawPath(Path path, Paint paint) //绘制一个路径,参数一为Path路径对象
            Path path = new Path();
            path.moveTo(50,500);   //设置起始点
            path.lineTo(100,720); //连接点
            path.lineTo(400,420); //连接点
            path.close();
            mPaint.setStyle(Paint.Style.FILL);    //画笔
            mPaint.setColor(Color.BLUE);
            canvas.drawPath(path,mPaint);
    
    
            //贴图
            //参数一就是我们常规的Bitmap对象,
            //参数二是源区域(这里是bitmap)
            //参数三是目标区域(应该在canvas的位置和大小)
            //参数四是Paint画刷对象
            //因为用到了缩放和拉伸的可能,当原始Rect不等于目标Rect时性能将会有大幅损失。
            //drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
            canvas.drawBitmap(mBitmap,500,500,mPaint);
    
            Rect src = new Rect(0,0,mBitmap.getWidth(),mBitmap.getHeight());  //想要绘制原图的哪部分区域
            RectF dst = new RectF(200,200,500,800);  //绘制的位置
            canvas.drawBitmap(mBitmap,src,dst,mPaint);
    
             Matrix matrix = new Matrix();
            matrix.reset();
    
            //缩放
    //        matrix.setScale(0.8f,0.8f);
            matrix.setRotate(30);
            canvas.drawBitmap(mBitmap,matrix,mPaint);
    
    
            //画线,
            //参数一起始点的x轴位置,
            //参数二起始点的y轴位置,
            //参数三终点的x轴水平位置,
            //参数四y轴垂直位置,
            //最后一个参数为Paint 画刷对象。
            //drawLine(float startX, float startY, float stopX, float stopY, Paintpaint)
            mPaint.setColor(Color.BLACK);
            mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            mPaint.setStrokeWidth(20);  //设置线宽
            canvas.drawLine(50f,50f,200f,200f,mPaint);
    
            //画点,参数一水平x轴,参数二垂直y轴,第三个参数为Paint对象。
            //drawPoint(float x, float y, Paint paint)
            canvas.drawPoint(500f,500f,mPaint);
    
            //渲染文本,Canvas类除了上面的还可以描绘文字,
            //参数一是String类型的文本,
            //参数二x轴,
            //参数三y轴,
            //参数四是Paint对象。
            //drawText(String text, float x, floaty, Paint paint)
            mPaint.setStrokeWidth(1);
            canvas.drawText("CD1605",800F,400F,mPaint);
    
            //方法,该方法可以沿着Path绘制文本
            // 其中hOffset参数指定水平偏移 (文本间距)
            // vOffset参数指定垂直偏移(距离线顶部的间距)
            canvas.drawTextOnPath("天行健,君子以自强不息",path,-20,80,mPaint);
    
            //画椭圆,
            //参数一是扫描区域 即 椭圆区域
            //参数二为paint对象;
            //drawOval(RectF oval, Paint paint)
            RectF oval = new RectF(300f,800f,600f,900f);
            canvas.drawOval(oval,mPaint);
            //API>=21
    //        canvas.drawOval(200f,500f,600f,800f,mPaint);
    
    
            // 绘制圆,
            // 参数一是中心点的x轴,
            // 参数二是中心点的y轴,
            // 参数三是半径,
            // 参数四是paint对象;
            //drawCircle(float cx, float cy, float radius,Paint paint)
            mPaint.setARGB(255,100,255,255);
            canvas.drawCircle(700,700,100,mPaint);
    
            //画弧
            //参数一是RectF对象,一个矩形区域椭圆形的界限用于定义在形状、大小、弧,
            //参数二是起始角(度)在电弧的开始,
            //参数三扫描角(度)开始顺时针测量的,
            //参数四是如果这是真的话,包括椭圆中心的电弧,并关闭它,如果它是假这将是一个弧线,
            //参数五是Paint对象;
            //drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
            RectF arc = new RectF(300f,1200f,600f,1400f);
            mPaint.setARGB(255,100,100,255);
            mPaint.setStyle(Paint.Style.STROKE);
            canvas.drawArc(arc,0,180,false,mPaint);
    
    
    
    
            // 线性渲染
            // 其中,参数x0表示渐变的起始点x坐标;
            // 参数y0表示渐变的起始点y坐标;
            // 参数x1表示渐变的终点x坐标;
            // 参数y1表示渐变的终点y坐标 ;
            // color0表示渐变开始颜色;
            // color1表示渐变结束颜色;
            // 参数tile表示平铺方式。
            Shader mShader = new LinearGradient(
                    0, 0, 100, 100,
                    new int[] { Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW,
                            Color.LTGRAY }, null, Shader.TileMode.REPEAT); // 一个材质,打造出一个线性梯度沿著一条线。
    
            mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            mPaint.setShader(mShader);
            RectF oval2 = new RectF(60, 100, 200, 240);// 设置个新的长方形,扫描测量
            canvas.drawArc(oval2, 200, 130, true, mPaint);
            // 画弧,第一个参数是RectF:该类是第二个参数是角度的开始,第三个参数是多少度,第四个参数是真的时候画扇形,是假的时候画弧线
    
    
            //画贝塞尔曲线
            mPaint.reset();
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setColor(Color.GREEN);
            mPaint.setStrokeWidth(20);
            Path path2=new Path();
            path2.moveTo(100, 320);//设置Path的起点
    
            // x1,y1为控制点的坐标值,x2,y2为终点的坐标值;
            // 贝塞尔曲线的形成,就比如我们把一条橡皮筋拉直,橡皮筋的头尾部对应起点和终点,
            // 然后从拉直的橡皮筋中选择任意一点(除头尾对应的点外)扯动橡皮筋形成的弯曲形状,
            // 而那个扯动橡皮筋的点就是控制点
    
            path2.quadTo(300, 310, 170, 400); //设置贝塞尔曲线的控制点坐标和终点坐标
    //        path2.close();//闭合绘制
            canvas.drawPath(path2, mPaint);//画出贝塞尔曲线
    
            //绘制 画布 
            //canvas.drawColor(Color.YELLOW);
            //清空 或者 
            //canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        }
    

    **4.onMeasure **
    为了正确的绘制你的view,你需要知道view的大小。复杂的自定义view通常需要根据在屏幕上的大小与形状执行多次layout计算。而不是假设这个view在屏幕上的显示大小。即使只有一个程序会使用你的view,仍然是需要处理屏幕大小不同,密度不同,方向不同所带来的影响。
    尽管view有许多方法是用来计算大小的,但是大多数是不需要重写的。如果你的view不需要特别的控制它的大小,唯一需要重写的方法是[onSizeChanged()](http://developer.android.com/reference/android/view/View.html#onSizeChanged(int, int, int, int)).
    onSizeChanged(),当你的view第一次被赋予一个大小时,或者你的view大小被更改时会被执行。在onSizeChanged方法里面计算位置,间距等其他与你的view大小值。
    当你的view被设置大小时,layout manager(布局管理器)假定这个大小包括所有的view的内边距(padding)。当你计算你的view大小时,你必须处理内边距的值。这段MyView.onSizeChanged()
    代码演示:

         // Account for padding
           float xpad = (float)(getPaddingLeft() + getPaddingRight());
           float ypad = (float)(getPaddingTop() + getPaddingBottom());
    
           // Account for the label
           if (mShowText) xpad += mTextWidth;
    
           float ww = (float)w - xpad;
           float hh = (float)h - ypad;
    
           // Figure out how big we can make the pie.
           float diameter = Math.min(ww, hh);
    

    如果你想更加精确的控制你的view的大小,需要重写[onMeasure()](http://developer.android.com/reference/android/view/View.html#onMeasure(int, int))方法。这个方法的参数是View.MeasureSpec,它会告诉你的view的父控件的大小。那些值被包装成int类型,你可以使用静态方法来获取其中的信息。

    /**
         * 布局测量
         * @param widthMeasureSpec
         * @param heightMeasureSpec
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            //MeasureSpec通常翻译为”测量规格”,它是一个32位的int数据.
            //其中高2位代表SpecMode即某种测量模式,低30位为SpecSize代表在该模式下的规格大小.
            //getMode 获取设置的模式AT_MOST、EXACTLY、UNSPECIFIED
        
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            int width=0,height=0;
    
            if(widthMode ==MeasureSpec.AT_MOST){
                //宽度类型为warp_content ,
                //此处如果布局设置为 warp_content我们将width设置为200PX(这个高度应该是你自己绘制的区域高度)
                width = 200;
            }else if(widthMode ==MeasureSpec.EXACTLY){
                //宽度类型为match_parent 或者定义好的dp值得,此处我们设置为控件测量获取的父容器所给予的宽度
                width = widthSize;
            }else if(widthMode ==MeasureSpec.UNSPECIFIED){
                //表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见,一般用不到
            }
    
            if(heightMode ==MeasureSpec.AT_MOST){
                //高度类型为warp_content
                //此处如果布局设置为 warp_content我们将height设置为200PX(这个高度应该是你自己绘制的区域高度)
                height = 200;
            }else if(heightMode ==MeasureSpec.EXACTLY){
                //高度类型为match_parent 或者定义好的dp值得,此处我们设置为控件测量获取的父容器所给予的高度
                height = heightSize;
            }else if(heightMode ==MeasureSpec.UNSPECIFIED){
                //表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见,一般用不到
            }
    
            //设置控件宽高
            setMeasuredDimension(width,height);
        }
    

    上面的代码有2个重要的事情需要注意:

    1.计算的过程有把view的最好把padding考虑进去。这部分是view所控制的。(此处的示例代码 没有给出)
    2.onMeasure()没有返回值。它通过调用setMeasuredDimension()来获取结果。调用这个方法是强制执行的,如果你遗漏了这个方法,会出现运行时异常。

    4.onTouchEvent给你的view添加触摸动作
    有些时候我们需要给我们的View添加一些手指按下,移动,抬起的操作,这时候就需要重写 View 的 onTouchEvent方法。

    /**
         * 事件触发
         * @param event
         * @return
         */
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    Log.e(TAG,"按下:("+event.getX()+":"+event.getY()+")");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG,"移动:("+event.getX()+":"+event.getY()+")");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e(TAG,"抬起:("+event.getX()+":"+event.getY()+")");
                    break;
            }
            //消费事件
            return true;
        }
    

    很多时候onTouch本身并不能满足我们的需求,比如快速滑动View时的速度等,这时候就需要借助另外一个Android给我们提供的另一个touch事件GestureDetector

    class mListener extends GestureDetector.SimpleOnGestureListener {
       @Override
       public boolean onDown(MotionEvent e) {
           return true;
       }
    }
    mDetector = new GestureDetector(PieChart.this.getContext(), new ```
    不管你是否使用GestureDetector.SimpleOnGestureListener, 你必须总是实现onDown()方法,并返回true。这一步是必须的,因为所有的gestures都是从onDown()开始的。如果你在onDown()里面返回false,系统会认为你想要忽略后续的gesture,那么GestureDetector.OnGestureListener的其他回调方法就不会被执行到了。一旦你实现了GestureDetector.OnGestureListener并且创建了GestureDetector的实例, 你可以使用你的GestureDetector来中止你在onTouchEvent里面收到的touch事件。
    

    Override
    public boolean onTouchEvent(MotionEvent event) {
    boolean result = mDetector.onTouchEvent(event);
    if (!result) {
    if (event.getAction() == MotionEvent.ACTION_UP) {
    stopScrolling();
    result = true;
    }
    }
    return result;
    }

    当你传递一个touch事件到onTouchEvent()时,若这个事件没有被辨认出是何种gesture,它会返回false。你可以执行自定义的gesture-decection代码。

    相关文章

      网友评论

        本文标题:Android 之 自定义控件 之 View

        本文链接:https://www.haomeiwen.com/subject/jlonsttx.html