Android群英传笔记-第3章 Android控件架构与自定义控件详解

1.Android控件架构

Android中的每个控件都会在界面中占得一块矩形得区域。上层控件负责下层子控件得测量和绘制,并传递交互事件。通常在Activity中使用findViewById()方法,就是在控件树中以树得深度优先遍历来查找对应元素。

在显示上,他将屏幕分成了两部分,一个title一个content,看到这里,大家应该能看到一个熟悉的界面ContentView,它是一个ID为content分framelayout,activity_main.xml就是设置在这个framelayout里面
这里写图片描述
这里写图片描述

而如果用户通过设置requestWindowFeature(Window.FEATURE_NO_TITLE)来设置全屏显示,视图树中的布局中就只有Content了,这就解释为什么requestWindowFeature()方法一定要在调用setContentView()方法之前才能生效的原因。

2.View的测量

我们想要绘制一个View,首先还是得知道这个View的大小,系统是如何把他绘制出来的,在Android中,我们要想绘制一个View,就必须要知道这个View的大小,然后告诉系统,这个过程在onMeasure()中进行

Android给我们提供了一个设计短小精悍的类——MeasureSpec类,通过他来帮助我们测量View, MeasureSpec是一个32位的int值,其中高2位为测量模式,低30为测量的大小,在计算中使用位运算时为了提高并且优化效率

测量模式

  • EXACTLY

    精准值模式:表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小

  • AT_MOST

    最大值模式:表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

  • UNSPECIFIED

    表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到

View默认的onMeasure只支持EXACTLY模式,所以如果在自定义控件的时候不重写这个方法的话,也就只能使用EXACTLY模式了,控件可以响应你制定的具体的宽高值或者match_parent属性,如果我们自定义View要让他支持 wrap_content,那就必须重写onMeasure指定wrap_content时的大小,

   /**
     * 测量
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

win下按住Ctrl查看super.onMeasure()这个方法,可以发现,系统最终还是会调用setMeasuredDimension()这个方法将测量的宽高设置进去从而完成测量工作

通过分析,重写onMeasure()方法代码如下所示(模板代码):

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
                measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec)
        );
    }
  • measureWidth(widthMeasureSpec)
private int measureWidth(int widthMeasureSpec) {
        int result = 0 ; //最终返回的大小
        int specMode = MeasureSpec.getMode(widthMeasureSpec) ; //获取测量模式
        int specSize = MeasureSpec.getSize(widthMeasureSpec) ; //获取测量大小

        if (specMode == MeasureSpec.EXACTLY){
            result = specSize ;              //如果为确定值,最终就返回确定值。
        }else {
            result = 200 ;  //默认的长度
            if (specMode == MeasureSpec.AT_MOST){
               //如果控件取wrap_content
                result = Math.min(result,specSize); //在父容器最大值和默认值中选最小,因为不能超过父容器最大值
            }
        }
        return result ;
    }

当指定宽高属性为wrap_content属性时,如果不重写onMeasure()方法,那么系统就不知道该使用默认
多大的尺寸。因此,它就会默认填充整个父布局,所以重写onMeasure()方法的目的,就是为了能够给
View一个wrap_content属性下的默认大小。

View的绘制

当测量好一个View之后,就可以重写onDraw()方法,并在Canvas对象上绘制所需要的图形。
Canvas就像是一个画板,使用Paint就可以在上面作画。
onDraw()中有一个参数,就是Canvas canvas 对象。使用这个Canvas对象就可以进行绘图。

ViewGroup的测量

ViewGroup会去管理其子View,其中一个管理项目就是负责子View的显示大小。

当ViewGround的大小为wrap_content时,ViewGroup就需要对子View进行遍历

以便获得所有子View的大小,从而来决定自己的大小。而在其他模式下则会通

过其具体的指定值来设置自身的大小。

ViewGroup遍历所有的子View会调用所有的子View的onMeasure()方法来获取测量结果,前面所说的对View的测量,就是在这里进行的。

当子View测量完毕之后,,就需要将子View放在合适的地方,这部分是由onLayout()来进行的

在我们自定义ViewGroup的时候,一般都要重写onLayout()方法控制子View显示位置的逻辑,同样,如果需要wrap_content属性,那就必须重写onLayout()方法了,这点和View是相同的

ViewGround的绘制

ViewGroup在一般情况下是不会绘制的,因为他本身没有需要绘制的东西,如果不是指定ViewGroup的背景颜色,他连onDraw()都不会调用,但是ViewGroup会使用dispatchDraw()来绘制其他子View,其过程同样是遍历所哟普的子View,并调用子View的绘制方法来完成绘制的

自定义View

在View中通常有以下比较重要的回调方法

  • onFinishInflate()
    //从XML加载组件后回调
    @Override
    protected void onFinishInflate() {
        // TODO Auto-generated method stub
        super.onFinishInflate();
    }
  • onSizeChanged()
    //组件大小改变时回调
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        // TODO Auto-generated method stub
        super.onSizeChanged(w, h, oldw, oldh);
    }
  • onMeasure()
    // 回调该方法进行测量
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // TODO Auto-generated method stub
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
  • onLayout()
   // 回调该方法来确定显示的位置
    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        // TODO Auto-generated method stub
        super.onLayout(changed, left, top, right, bottom);
    }
  • onTouchEvent()
    // 监听到触摸时间时回调
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // TODO Auto-generated method stub
        return super.onTouchEvent(event);
    }
  • onDraw()
    // 绘图
    @Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);
    }

通常情况下,有以下三种方法来实现自定义的控件

  • 对现有的控件进行扩展
  • 通过组件来实现新的控件
  • 重写View来实现全新的控件

对现有的控件进行扩展

一般来说,我们可以在onDraw()方法中对原生控件行为进行拓展

  • getWidth(): View在設定好佈局後整個View的寬度。
  • getMeasuredWidth(): 對View上的內容進行測量後得到的View內容佔據的寬度

这里写图片描述

代码:

package com.lgl.viewdemo;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.widget.TextView;

/**
 * 自定义TextView
 * Created by lgl on 16/3/4.
 */
public class CosuTextView extends TextView {

    private Paint paint1, paint2;

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

    private void init() {
        //实例化画笔1
        paint1 = new Paint();
        //设置颜色
        paint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
        //设置style
        paint1.setStyle(Paint.Style.FILL);

        //同上
        paint2 = new Paint();
        paint2.setColor(Color.YELLOW);
        paint2.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //绘制外层
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint1);
        //绘制内层
        canvas.drawRect(10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, paint2);

        canvas.save();
        //绘制文字前平移10像素
        canvas.translate(10, 0);
        //父类完成方法
        super.onDraw(canvas);
        canvas.restore();
    }
}
onDraw调用和android中Invalidate和postInvalidate的区别
  • postInvalidate()是重绘的,也就是调用postInvalidate()后系统会重新调用onDraw方法画一次

  • Android中实现view的更新有两组方法,一组是invalidate,另一组是postInvalidate,其中前者是在UI线程自身中使用,而后者在非UI线程中使用。

创建复合控件

创建一个复核人控件可以很好的创建出具有重要功能的控件集合,这种方式经常需要继承一个合适的ViewGroup,再给他添加指定功能的控件,从而组成一个新的合适的控件,通过这种方式创建的控件,我们一般都会给他指定的一些属性,让他具有更强的扩展性,下面就以一个TopBar为例子,讲解如何创建复合控件

定义属性

我们需要给他定义一些属性,这样的话,我们需要在values下新建一个attrs.xml文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TopBar">
        <attr name="title" format="string" />
        <attr name="titleTextSize" format="dimension" />
        <attr name="titleTextColor" format="color" />
        <attr name="leftTextColor" format="color" />
        <attr name="leftBackground" format="reference|color" />
        <attr name="leftText" format="string" />
        <attr name="rightTextColor" format="color" />
        <attr name="rightBackground" format="reference|color" />
        <attr name="rightText" format="string" />
    </declare-styleable>
</resources>

我们在代码中是可以用< declare-styleable >标签去声明一些属性的,然后name相当于ID让我们的类可以找到

   /**
     * 初始化属性
     * @param attrs
     */
    private void initAttrs(Context context,AttributeSet attrs){
        //通过这个方法,你可以从你的attrs.xml文件下读取读取到的值存储在你的TypedArray
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBarView);

        //读取出相应的值设置属性
        mLeftTextColor = ta.getColor(R.styleable.TopBarView_leftTextColor, 0);
        mLeftBackground = ta.getDrawable(R.styleable.TopBarView_leftBackground);
        mLeftText = ta.getString(R.styleable.TopBarView_leftText);

        mRightTextColor = ta.getColor(R.styleable.TopBarView_rightTextColor, 0);
        mRightBackgroup = ta.getDrawable(R.styleable.TopBarView_rightBackground);
        mRightText = ta.getString(R.styleable.TopBarView_rightText);

        mTitleSize = ta.getDimension(R.styleable.TopBarView_titleTextSize, 10);
        mTitleColor = ta.getColor(R.styleable.TopBarView_titleTextColor, 0);

        mTitle = ta.getString(R.styleable.TopBarView_title);

        //获取完TypedArray的值之后,一般要调用recyle方法来避免重复创建时候的错误
        ta.recycle();
    }

当获取完所有的属性值后,需要调用TypedArray的recycle方法来完成资源的回收。

组合控件

通过动态添加控件的方式,使用addView()方法将这三个控件加入到定义的TopBar模板中,
并给它们设置我们前面所获取到的具体属性值,比如标题的文字颜色,大小等。

  • 组合控件
    这里写图片描述
    这里写图片描述

  • 定义接口 :在UI模板类中定义一个左右按钮点击的接口,并创建两个方法,分别用于左边按钮点击和右边按钮的点击
    这里写图片描述

  • 暴露接口给调用者 :在模板方法中,为左,右按钮增加点击事件,但不去实现具体的逻辑,而是调用接口中相应的点击方法
    这里写图片描述

  • 实现接口回调
    这里写图片描述



重写View来实现全新的控件

弧形展示图

这里写图片描述

/**
 * 半弧圆
 * Created by lgl on 16/3/7.
 */
public class CircleView extends View {

    //圆的长度
    private int mCircleXY;
    //屏幕高宽
    private int w, h;
    //圆的半径
    private float mRadius;
    //圆的画笔
    private Paint mCirclePaint;
    //弧线的画笔
    private Paint mArcPaint;
    //文本画笔
    private Paint mTextPaint;
    //需要显示的文字
    private String mShowText = "Android";
    //文字大小
    private int mTextSize = 50;
    //圆心扫描的弧度
    private int mSweepAngle = 270;


    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //获取屏幕高宽
        WindowManager wm = (WindowManager) getContext()
                .getSystemService(Context.WINDOW_SERVICE);
        w = wm.getDefaultDisplay().getWidth();
        h = wm.getDefaultDisplay().getHeight();


        init();
    }

    private void init() {
        mCircleXY = w / 2;
        mRadius = (float) (w * 0.5 / 2);

        mCirclePaint = new Paint();
        mCirclePaint.setColor(Color.BLUE);

        mArcPaint = new Paint();
        //设置线宽
        mArcPaint.setStrokeWidth(100);
        //设置空心
        mArcPaint.setStyle(Paint.Style.STROKE);
        //设置颜色
        mArcPaint.setColor(Color.BLUE);

        mTextPaint = new Paint();
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setTextSize(mTextSize);

    }

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

        //绘制矩形
        RectF mArcRectF = new RectF((float) (w * 0.1), (float) (w * 0.1), (float) (w * 0.9), (float) (w * 0.9));
        //绘制圆
        canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
        //绘制弧线
        canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);
        //绘制文本
        canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY + (mTextSize / 4), mTextPaint);
    }
}

动态刷新:

 public void setSweepValues(float sweepValues){
        if(sweepValues !=- 0){
            mSweepAngle = sweepValues;
        }else{
            //如果没有,我们默认设置
            mSweepAngle = 30;
        }
        //记得刷新哦
        invalidate();
    }
音频条形图

如何实现动态效果,只要在ondraw()方法中再去调用invalidate()方法通知View进行重绘就可以了,不过有时候并不需要每一次绘制完新的矩形就通知View进行重绘,这样会因为刷新速度太快反而影响效果,需要来进行View的延时重绘

postInvalidateDelayed(300);

关于着色器LinearGradient的使用

这里写图片描述

package com.liguangjie.practice.View;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;

/**
 * Created by hasee on 2017/1/10.
 */
public class audioView extends View {

    private int w ,h ;

    private Paint mPaint ;

    private int mRectCount = 12 ;

    //每一条要有间隔
    private int offset =2 ;

    private int mRectHeight = 1700 ; //初始化假数据

    private float currentHeight ; //当前的高度

    private double mRandom ;

    private int mRectWidth ;

    private int beginwidth ;

    private LinearGradient mLinearGradient ;

    public audioView(Context context) {
        this(context,null);
    }

    public audioView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public audioView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取屏幕高宽
        WindowManager wm = (WindowManager) getContext()
                .getSystemService(Context.WINDOW_SERVICE);
        w = wm.getDefaultDisplay().getWidth();
        h = wm.getDefaultDisplay().getHeight();
        init();
    }

    private void init(){
        mRectWidth = (int)(w*0.6/12-offset);
        beginwidth = (int)(w*0.2);
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for(int i=0;i<12;i++){
            canvas.drawRect(beginwidth+i*(mRectWidth+offset),getRandom(),beginwidth+i*(mRectWidth+offset)+mRectWidth ,h,mPaint);
        }
        postInvalidateDelayed(300);
    }


    //获取随机数
    public float getRandom(){
        mRandom = Math.random();
        currentHeight = (float)(mRectHeight*mRandom);
        return currentHeight;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //颜色渐变
        mLinearGradient = new LinearGradient(0,0,mRectWidth,getHeight(),Color.YELLOW,Color.BLUE, Shader.TileMode.CLAMP);
        mPaint.setShader(mLinearGradient);
    }
}

自定义ViewGroup

通常我们自定义ViewGroup是需要onMeasure()来测量的,然后重写onLayout()来确定位置,重写onTouchEvent()来相应事件

例子:实现一个类似Android原生控件ScrollView的自定义ViewGroup自定义ViewGroup可以实现ScrollView所具有的上下滑动功能,但是在滑动的过程中,增加一个粘性的效果。

  • ViewGroup的高度是屏幕高度,不要局限与屏幕

首先先放置好它的子View,使用遍历的方式来通知子View对自身进行测量。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        for(int i=0;i<count;i++){
            View childView = getChildAt(i);
            measureChild(childView,widthMeasureSpec,heightMeasureSpec);
        }
    }

确定ViewGroup的宽高

       int childCount = getChildCount(); //获取子View个数
        //设置ViewGroup的高度
        MarginLayoutParams mlp = (MarginLayoutParams)getLayoutParams();
        mlp.height = mScreenHeight * childCount ;
        setLayoutParams(mlp);

遍历子View的layout()方法,并将具体的位置作为参数传递进来

     //@param changed 该参数指出当前ViewGroup的尺寸或者位置是否发生了改变
     //@param left top right bottom 当前ViewGroup相对于其父控件的坐标位置
    @Override
    protected void onLayout(boolean b, int left, int top, int right, int bottom) {
       int childCount = getChildCount(); //获取子View个数
        //设置ViewGroup的高度
        MarginLayoutParams mlp = (MarginLayoutParams)getLayoutParams();
        mlp.height = mScreenHeight * childCount ;
        setLayoutParams(mlp);

        for(int a =0;a<childCount;a++){
            View child = getChildAt(a);
            if(child.getVisibility()!= View.GONE){
                child.layout(left,a*mScreenHeight,right,(a+1)*mScreenHeight);
            }

        }
    }

重写onTouchEvent()方法响应触摸事件

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值