1)Android控件
1)控件树
在Android中每个控件都占有一块矩形区域,控件一般分两类,View和ViewGroup,ViewGroup作为父控件可以包含多个子控件,并管理其包含的View控件。通过ViewGroup整个界面上的控件形成了一个树形结构,也就是常说的控件树,上层控件负责下层控件的测量和绘制,并传递交互事件,通常在Activity中使用findViewById()方法,就是在控件树中以树的深度优先遍历来查找对应元素。每个树的顶部都有一个DecorView对象,所有的交互事件都由它来统一调度和分配,从而可以对整个视图进行整体控制。下图展示了其树形结构.
2)UI界面架构图
Activity:基本的页面单元,Activity包含一个Window,window上可以绘制各种view
Window:表示顶层窗口,管理界面的显示和事件的响应;每个Activity均会创建一个
PhoneWindow对象,是Activity和整个View系统交互的接口,是Window的具体实现。PhoneWindow类内部包含了一个DecorView对象。简而言之,PhoneWindow是把一个FrameLayout进行了一定的包装,并提供了一组通用的窗口操作接口。
DecorView:是Window中View的RootView,设置窗口属性;该类是一个FrameLayout的子类,并且是PhoneWindow中的一个内部类。Decor的英文是Decoration,即“修饰”的意思,DecorView就是对普通的FrameLayout进行了一定的修饰,比如添加一个通用的Titlebar,并响应特定的按键消息等。
ViewRoot:它并不是一个View类型,而是一个Handler。
它的主要作用如下:
A. 向DecorView分发收到的用户发起的event事件,如按键,触屏,轨迹球等事件;
B. 与WindowManagerService交互,完成整个Activity的GUI的绘制。
3)绘制流程
整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简单概况为
根据之前设置的状态,判断是否需要重新计算视图大小(measure)、是否重新需要安置视图的位置(layout)、以及是否需要重绘 (draw)。
2)View的测量
在Android中控件不会无缘无故出现在屏幕上,其内部必然经过大量的测量,计算布局和绘制,才会显示在界面上,而作为自定义控件中最重要的步骤测量也是最重要一环。
先来了解下几个重要概念
MeasureSpec 它是来帮助我们测量View的一个类,MeasureSpec是一个32位的int值,其中高2位为测量模式,低30位为测量的大小,通过这个可以获取测量模式和大小。
三种测量模式
EXACTLY
精确测量模式,当我们在布局中将宽高设置为具体数值或match_parent(占据父View的大小)时,系统使用这种测量模式。
AT_MOST
最大值模式,当控件的宽高设置为wrap_content时,此时控件大小随着子控件或内容的变化而变化,此时控件大小只要不超过父控件运行的大小就可以了。
UNSPECIFIED
不指定其大小测量模式,View想多大就多大,通常在绘制自定义View时使用,一般不用,可以不用管。
要测量,需要重写onMeasure()
看下一般测量流程的模板代码
可以看到先通过 MeasureSpec对象中取出测量的模式和大小,通过测量的模式,给出不同的测量值。当SpecMode为EXACTLY,就使用用户给定的测量大小,其他两种,需要给定一个默认的测量大小,当为AT_MOST时,需要取出系统测量值与默认值的最小值,这就是其大致测量模式。
3)View的绘制
当测量好了,我们就可以绘制了,我们需要重写onDraw()方法,
具体的绘制流程还是需要看不同的需求的,这后面再说。
4)ViewGroup的测量
ViewGroup管理其子View,其中负责子View的显示大小。
测量:ViewGroup的大小为wrap_content(即AT_MOST)时,ViewGroup对子View遍历,获取所有子View的大小(调用子View的Measure方法获取测量结果),从而决定自己的大小。其他模式下会通过具体的指定值来设置大小。
Layout过程:测量后需要将子View放到合适的位置。遍历子View的Layout方法,并制定其具体显示的位置,从而来具体决定其布局位置(可以重写onLayout()来控制子View的显示位置的逻辑,如果需要支持wrap_content属性,需重写onMeasure())。
5)ViewGroup的绘制
ViewGroup通常情况是不需要绘制的。 当指定背景颜色时,onDraw方法会被调用。 ViewGroup可以通过dispatchDraw方法来(遍历调用子View的onDraw方法)绘制子View。
6)自定义View
自定义View中通常用到的回调方法
onFinishInflate(): xml加载完毕之后回调
onSizeChanged(): 组件大小改变之后回调。
onMeasure(): 组件测量
onLayout(): 确定ViewGroup中子view的位置
onTouchEvent(): 监听触摸事件回调
onDraw(): 最常用,绘制View
一般有三种方法来实现自定义的控件:
1) 对现有组件进行拓展
2) 通过组合来实现新的控件
3) 重写View来实现全新的控件
7)重写View来实现全新的控件
前面两种就不多说了,蛮容易懂得,看下《Android群英传》就可以懂了
结合案例来说下使用
1)弧线展示图
package com.chen.demo.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.WindowManager;
/**
* Created by chenxiaokang on 2016/12/1.
*/
public class MyView extends View {
private int length;
private int mCircleXY;
private float mRadius;
private RectF mArcRectF;
private int mSweepAngle;
private Paint mPaint;
private Paint mArcPaint;
private Paint mTextPaint;
private String mText;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mSweepAngle = 187;
//绘制圆形的画笔
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
mPaint.setColor(Color.GREEN);
//绘制弧形的画笔
mArcPaint = new Paint();
mArcPaint.setAntiAlias(true);
mArcPaint.setStyle(Paint.Style.STROKE);
mArcPaint.setStrokeWidth(24);
mArcPaint.setColor(Color.RED);
//绘制文字的画笔
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setColor(Color.BLUE);
mTextPaint.setTextSize(100);
mTextPaint.setTextAlign(Paint.Align.CENTER);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getRealMetrics(metrics);
length = Math.min(metrics.widthPixels, metrics.heightPixels) - 20;
//中间的圆
mCircleXY = length/2;
mRadius = length*0.5f/2;
//弧线
mArcRectF = new RectF(length*0.1f, length*0.1f, length*0.9f, length*0.9f);
mText = "全自定义View";
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(length, length);
}
@Override
protected void onDraw(Canvas canvas) {
//绘制内部圆
canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mPaint);
//绘制弧线
canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);
//绘制文字
int textHeight = (int) (mTextPaint.descent()-mTextPaint.ascent());
canvas.drawText(mText, 0, mText.length(), mCircleXY, mCircleXY+textHeight/4, mTextPaint);
}
}
效果:
我们通过自定义View绘制了三个部分,内部圆,弧线,和文字,绘制了一个全新的控件。
2)音频图
package com.chen.demo.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.DisplayMetrics;
import android.view.View;
import android.view.WindowManager;
/**
* Created by chenxiaokang on 2016/12/1.
*/
public class MyViewB extends View{
private int mRectCount = 30;
private int width;
private int height;
private int currentHeight;
private int offset = 3;
private int mRectWidth = 20;
private int mStartPoint;
private Paint paint;
public MyViewB(Context context) {
this(context, null);
}
public MyViewB(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyViewB(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getRealMetrics(metrics);
width = metrics.widthPixels;
height = (int) (metrics.heightPixels*1.0f*4/5);
currentHeight = (int) (height*1.0f*3/5);
mStartPoint = (width - mRectCount*mRectWidth)/2;
paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
LinearGradient linearGradient = new LinearGradient(0, 0, width, height, Color.RED, Color.GREEN, Shader.TileMode.CLAMP);
paint.setShader(linearGradient);
}
@Override
protected void onDraw(Canvas canvas) {
for(int i = 0; i<mRectCount; i++){
currentHeight = (int) (Math.random()*height);
canvas.drawRect(mStartPoint+offset+mRectWidth*i, height-currentHeight,
mStartPoint+mRectWidth*(i+1), height, paint);
}
postInvalidateDelayed(200);
}
}
效果:
就是那种音频跳动的效果
8)自定义ViewGroup
package com.chen.demo.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Scroller;
/**
* Created by chenxiaokang on 2016/12/2.
*/
public class MyViewGroup extends ViewGroup{
int width, height;
Scroller mScroller;
public MyViewGroup(Context context) {
this(context, null);
}
public MyViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getRealMetrics(metrics);
width = metrics.widthPixels;
height = metrics.heightPixels;
mScroller = new Scroller(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i<count; i++){
View view = getChildAt(i);
measureChild(view, width, height);
}
}
//放置每个子view的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
MarginLayoutParams mlp = (MarginLayoutParams)getLayoutParams();
mlp.height = height * childCount;
setLayoutParams(mlp);
for(int i = 0; i<childCount; i++){
View view = getChildAt(i);
if(view.getVisibility() != View.GONE){
view.layout(l, i*height, r, (i+1)*height);
}
}
}
int mLastY;
int mStart;
int mEnd;
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mLastY = y;
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE:
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
int dy = mLastY - y;
if(getScrollY()<0){
dy = 0;
}
if(getScrollY() > getHeight()-height){
dy = 0;
}
scrollBy(0, dy);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
if(dScrollY > 0){
if(dScrollY < height/3){
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
}else {
mScroller.startScroll(0, getScrollY(), 0, height - dScrollY);
}
}else {
if(-dScrollY < height/3){
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
}else {
mScroller.startScroll(0, getScrollY(), 0, -height-dScrollY);
}
}
break;
}
postInvalidate();
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}
}
效果:
9)总结
我们可以看见,自定义View并不是很难,只要安装基本流程一步步来,终将可以写出nb的控件,再厉害的控件都是由最基本的组件组成的,只要熟练掌握好这些控件的使用方法,难得也就不再难了。
10)Ref
1)《Android群英传》
2) http://www.cnblogs.com/cowboybusy/archive/2012/08/26/2718888.html
3)http://blog.csdn.net/guolin_blog/article/details/17357967