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);
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()方法响应触摸事件