自定义控件分类:
1、使用系统控件,实现自定义的效果
2、自己定义一个类继承View ,如textView、ImageView等,通过重写相关的方法来实现新的效果
3、自己定义一个类继承ViewGroup,实现相应的效果
继承view类或viewgroup类,来创建所需要的控件。一般来讲,通过继承已有的控件来自定义控件要简单一点。
介绍下实现一个自定义view的基本流程
1.明确需求,确定你想实现的效果。
2.确定是使用组合控件的形式还是全新自定义的形式,组合控件即使用多个系统控件来合成一个新控件,你比如titilebar,这种形式相对简单。
3.如果是完全自定义一个view的话,你首先需要考虑继承哪个类,是View呢,还是ImageView等子类。
4.根据需要去复写View#onDraw、View#onMeasure、View#onLayout方法。
5.根据需要去复写dispatchTouchEvent、onTouchEvent方法。
6.根据需要为你的自定义view提供自定义属性,即编写attr.xml,然后在代码中通过TypedArray等类获取到自定义属性值。
7.需要处理滑动冲突、像素转换等问题。
绘制流程
onMeasure测量view的大小,设置自己显示在屏幕上的宽高。
onLayout确定view的位置,父view 会根据子view的需求,和自身的情况,来综合确定子view的位置(确定他的大小)。
onDraw(Canvas)绘制 view 的内容。
在主线程中 拿到view调用Invalide()方法,刷新当前视图,导致执行onDraw执行,如果是在子线程用postinvalidate,或者不需要一直刷新用postinvalidateDelayed(300),每隔300毫秒刷新一次。
如果希望视图的绘制流程(三步)可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了。
事件冲突
当点击事件发生时,事件最先传递给Activity,Activity会首先将事件将被所属的Window进行处理,即调用superDispatchTouchEvent()方法。
通过观察superDispatchTouchEvent()方法的调用链,我们可以发现事件的传递顺序:
事件一层层传递到了ViewGroup里。
当事件出现时,先从顶级开始往下传递,每到一个子view,看他的onInterceptTouchEvent 方法是否拦截,ontouch是否消费方法,如果没有继续向下dispatchTouchEvent分发事件,都不处理向上传,当回到顶级,若顶层(activity)也不对此事件进行处理,此事件相当于消失了(无效果)。
View没有onInterceptTouchEvent()方法,一但有点击事件传递给它,它的ouTouchEvent()方法就会被调用。
当事件发现冲突的时候,处理的原则就是事件分发机制,有俩种方法:
Activity/Window/View三者的差别,Activity 如何显示到屏幕上
ActivityManager :用于维护与管理 Activity 的启动与销毁
WindowManagerService:用来创建、管理和销毁Window。
Activity像一个工匠(控制单元),Window像窗户(承载模型),View像窗花(显示视图) LayoutInflater像剪刀,Xml配置像窗花图纸。
ActivityThread,Ams,Wms的工作原理
ActivityThread: 运行在应用进程的主线程上,响应 ActivityMananger、Service 启动、暂停Activity,广播接收等消息。
Ams:统一调度各应用程序的Activity、内存管理、进程管理。
自定义控件有几个重要方法:
1、实现构造方法 。(三个构造方法)
第二个是创建布局文件调用的构造函数
2、onMeasure测量view的大小。 设置自己显示在屏幕上的宽高。
MeasureSpec有SpecMode和SpecSize俩个属性。对于普通view,其MeasureSpec是由父容器的MeasureSpec和自身的layoutparams共同决定的,那么针对不同的父容器和view不同layoutparams,view可以有多种不同的MeasureSpec。
SpecMode有三类。
unspecified:父View不对子View做任何限制,需要多大给多大,一般不关心这个模式
exactly:view的大小就是SpecSize指定的大小。相当于mach_parents和具体数值
at_most:父容器指定了一个specsize,view不能大于这个值。具体的值看view,相当于wrap_content
日常开发中我们接触最多的不是MeasureSpec而是LayoutParams,在View测量的时候,LayoutParams会和父View的MeasureSpec相结合被换算成View的MeasureSpec,进而决定View的大小。
重写onMeasure为了测量view的大小, 设置自己显示在屏幕上的宽高。
如果写的自定义View是直接继承View的,而且写了super.measure(),则会默认给这个View设置了一个测量宽和高,这个宽高是多少?
//如果View没有设置背景,那么返回android:minWidth这个属性的值,这个值可以为0
//如果View设置了背景,那么返回android:minWidth和背景最小宽度两者中的最大值。
如果写的自定义View是继承现有控件的,而且写了super.measure(),则会默认使用那个现有控件的测量宽高,你可以在这个已经测量好的宽高上做修改,当然也可以全部重新测过再改掉。
如果我们的View直接继承ImageView,ImageView已经运行了一大堆已经写好的代码测出了相应的宽高。我们可以在它基础上更改即可。比如我们的Image2View是一个自定义的正方形的ImageView:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这里已经帮我们测好了ImageView的规则下的宽高,并且通过了setMeasuredDimension方法赋值进去了。
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//我们这里通过getMeasuredWidth/Height放来获取已经赋值过的测量的宽和高
//然后在ImageView帮我们测量好的宽高中,取小的值作为正方形的边。
//然后重新调用setMeasuredDimension赋值进去覆盖ImageView的赋值。
//我们从头到位都没有进行复杂测量的操作,全靠ImageView。哈哈
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if (width < height) {
setMeasuredDimension(width, width);
} else {
setMeasuredDimension(height, height);
}
}
3、onLayout设置自己显示在屏幕上的位置(只有在自定义ViewGroup中才用到),这个坐标是相对于当前视图的父视图而言的。view自身有一些建议权,决定权在 父view手中。
调用场景:在view需要给其孩子设置尺寸和位置时被调用。子view,包括孩子在内,必须重写onLayout(boolean, int, int, int, int)方法,并且调用各自的layout(int, int, int, int)方法。
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i); // 取得下标为I的子view
/**
* 父view 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小)
*/
//指定子view的位置 , 左,上,右,下,是指在viewGround坐标系中的位置
view.layout(0+i*getWidth(), 0, getWidth()+i*getWidth(), getHeight());
}
}
4、onDraw(Canvas)绘制 view 的内容。控制显示在屏幕上的样子(自定义viewgroup时不需要这个)
/*
* backgroundBitmap 要绘制的图片
* left 图片的左边界
* top 图片的上边界
* paint 绘制图片要使用的画笔
*/
canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
View和ViewGroup的区别
scrollTo()和scrollBy()
scrollTo:将当前视图的基准点移动到某个点(坐标点);
ScrollBy移动当前view内容 移动一段距离。
getHeight()和getMeasuredHeight()的区别:
有俩种方法可以获得控件的宽高
获取测量完的高度,只要在onMeasure方法执行完,就可以用它获取到宽高,在自定义view内使用view.measure(0,0)方法可以主动通知系统去测量,然后就可以直接使用它获取宽高。measure里调用的onmeasure
view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
headerView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int headerViewHeight = headerView.getHeight();
//直接可以获取宽高
}
});
这俩个一般情况是一样的,但是在viewgroup里getWidth是父类给子view分配的空间:右边-左边。系统可能需要多次measure才能确定最终的测量宽高,很可能是不准确的,好习惯是在onlayout里获得测量宽高或最终宽高。
还有一种获得控件宽高的方法:
onSizeChanged:当该组件的大小被改变时回调此方法
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 当尺寸有变化的时候调用
mHeight = getMeasuredHeight();
mWidth = getMeasuredWidth();
// 移动的范围
mRange = (int) (mWidth * 0.6f);
}
onFinishInflate
当xml被填充完毕时调用,在自定义viewgroup中,可以通过这个方法获得子view对象
protected void onFinishInflate() {
super.onFinishInflate();
// 容错性检查 (至少有俩子View, 子View必须是ViewGroup的子类)
if(getChildCount() < 2){
throw new IllegalStateException("布局至少有俩孩子. Your ViewGroup must have 2 children at least.");
}
if(!(getChildAt(0) instanceof ViewGroup && getChildAt(1) instanceof ViewGroup)){
throw new IllegalArgumentException("子View必须是ViewGroup的子类. Your children must be an instance of ViewGroup");
}
mLeftContent = (ViewGroup) getChildAt(0);
mMainContent = (ViewGroup) getChildAt(1);
}
其他概念
事件分发
View中 setOnTouchListener的onTouch,onTouchEvent,onClick的执行顺序
追溯到View的dispatchTouchEvent源码查看,有这么一段代码
public boolean dispatchTouchEvent(MotionEvent event) {
if (!onFilterTouchEventForSecurity(event)) {
return false;
}
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
}
当以下三个条件任意一个不成立时,
函数会执行到onTouchEvent。在这里我们可以看到,首先执行的是mOnTouchListener.onTouch的方法,然后是onTouchEvent方法
继续追溯源码,到onTouchEvent()观察,发现在处理ACTION_UP事件里有这么一段代码
if (!post(mPerformClick)) { performClick(); }
此时可知,onClick方法也在最后得到了执行
所以三者的顺序是:
view的事件分发:View为啥会有dispatchTouchEvent方法?
View可以注册很多事件监听器,事件的调度顺序是onTouchListener> onTouchEvent>onLongClickListener> onClickListener
View的事件分发
当事件出现时,先从顶级父类开始往下传递,每到一个孩子,看他的onInterceptTouchEvent 方法是否拦截,ontouch是否消费方法,如果没有继续向下dispatchTouchEvent分发事件,都不处理回到顶级的父空间,若顶层(activity)也不对此事件进行处理,此事件相当于消失了(无效果)。
Touchevent 中,返回值是 true ,则说明消耗掉了这个事件,返回值是 false ,则没有消耗掉,会继续传递下去
1)dispatchTouchEvent:这个方法用来分发事件,如果拦截了交给ontouchevent处理,对应上面的和ontounch理解,否则传给子view
2)onInterceptTouchEvent: 这个方法用来拦截事件,返回true表示拦截(不允许事件继续向子view传递),false不拦截,如果自定义viewgroup里某个子view需要自己处理事件,就需要重写改方法,让他返回false。
3)onTouchEvent: 这个方法用来处理事件。Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。,子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。
onTouchEvent
一般自定义控件都需要去重写onTouchEvent方法。
1.在down的时候去记录坐标点
getX/getY获取相对于当前View左上角的坐标,getRawX/getRawY获取相对于屏幕左上角的坐标。
比如接触到按钮时,x,y是相对于该按钮左上点的相对位置。而rawx,rawy始终是相对于屏幕的位置。
2.move的时候计算偏移量,并用scrollTo()或scrollBy()方法移动view。这俩个方法都是快速滑动,是瞬间移动的。
注意:滚动的并不是viewgroup内容本身,而是它的矩形边框。
三种滑动的方法
如果让view在一段时间内移动到某个位置(不是快速滑动,弹性)方法:
a.使用自定义动画(让view在一段时间内做某件事),extends Animation,
总要修改的是translationx.y这俩个值
(相对于父容器移动的距离)
b.使用Scoller
c.offsetTopAndBottom(offset)和offsetLeftAndRight(offset);,这个好理解,左右移动多少
模板(固定代码):
* @param startX 开始时的X坐标
* @param startY 开始时的Y坐标
* @param disX X方向 要移动的距离
* @param disY Y方向 要移动的距离
myScroller.startScroll(getScrollX(),0,distance,0,Math.abs(distance));//持续的时间
/**
* Scroller不主动去调用这个方法
* 而invalidate()可以掉这个方法
* invalidate->draw->computeScroll
*/
@Override
public void computeScroll() {
super.computeScroll();
if(scroller.computeScrollOffset()){//返回true,表示动画没结束
scrollTo(scroller.getCurrX(), 0);
invalidate();
}
}
scroller的工作原理:scroller本身并不能实现view的滑动,它需要配合view的的comouteScroll方法才能完成弹性滑动的效果,它不断的让view重绘,而每一次重绘距滑动起始时间会有有一个时间间隔,通过这个时间间隔srcoller就可以得出view当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成view的滑动,就这样,view的每一次重绘就会导致view进行小幅度的滑动,而多次的小幅度滑动就组成了弹性动画。
3.在up的时候,判断应显示的页面位置,并计算距离、滑动页面。见下:
ontouch触摸事件也可以交给其他工具类去实现
1.GestureDetector(手势识别器)去处理,可以在onFling里处理快速滑动事件,同时在MotionEvent.ACTION_UP里处理没有快速滑动的时候。有时候比ontounch更方便,比如处理onfling,onscroll(按下屏幕后拖动),长安,双击等事件。
mDectector.onTouchEvent(event);// 委托手势识别器处理触摸事件
...
case MotionEvent.ACTION_UP:
if(!isFling){// 在没有发生快速滑动的时候,才执行按位置判断currid
int nextId = 0;
if(event.getX()-firstX>getWidth()/2){ // 手指向右滑动,超过屏幕的1/2 当前的currid - 1
nextId = currId-1;
}else if(firstX - event.getX()>getWidth()/2){ // 手指向左滑动,超过屏幕的1/2 当前的currid + 1
nextId = currId+1;
}else{
nextId = currId;
}
moveToDest(nextId);
// scrollTo(0, 0);
}
isFling = false;
break;
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
//移动屏幕
/**
* 移动当前view内容 移动一段距离
* disX X方向移的距离 为正是,图片向左移动,为负时,图片向右移动
* disY Y方向移动的距离
*/
scrollBy((int) distanceX, 0);
return false;
}
@Override
/**
* 发生快速滑动时的回调
*/
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
isFling = true;
if(velocityX>0 && currId>0){ // 快速向右滑动。当前子view的下标
currId--;
}else if(velocityX<0 && currId<getChildCount()-1){ // 快速向左滑动
currId++;
}
moveToDest(currId);
return false;
}
@Override
public boolean onDown(MotionEvent e) {
return false;
}
});
2. 交给ViewDragHelper去处理
用法:
// a.初始化 (通过静态方法)
mDragHelper = ViewDragHelper.create(this , mCallback);
// b.传递触摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 传递给mDragHelper
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
mDragHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
// 返回true, 持续接受事件
return true;
}
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
// c. 重写事件
// 1. 根据返回结果决定当前child是否可以拖拽
// child 当前被拖拽的View
// pointerId 区分多点触摸的id
@Override
public boolean tryCaptureView(View child, int pointerId) {
Log.d(TAG, "tryCaptureView: " + child);
return true;
};
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
Log.d(TAG, "onViewCaptured: " + capturedChild);
// 当capturedChild被捕获时,调用.
super.onViewCaptured(capturedChild, activePointerId);
}
@Override
public int getViewHorizontalDragRange(View child) {
// 返回拖拽的范围, 不对拖拽进行真正的限制. 仅仅决定了动画执行速度
return mRange;
}
// 2. 根据建议值 修正将要移动到的(横向)位置 (重要)
// 此时没有发生真正的移动
public int clampViewPositionHorizontal(View child, int left, int dx) {
// child: 当前拖拽的View
// left 新的位置的建议值, dx 位置变化量
// left = oldLeft + dx;
Log.d(TAG, "clampViewPositionHorizontal: "
+ "oldLeft: " + child.getLeft() + " dx: " + dx + " left: " +left);
if(child == mMainContent){
left = fixLeft(left);
}
return left;
}
// 3. 当View位置改变的时候, 处理要做的事情 (更新状态, 伴随动画, 重绘界面)
// 此时,View已经发生了位置的改变
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
// changedView 改变位置的View
// left 新的左边值
// dx 水平方向变化量
super.onViewPositionChanged(changedView, left, top, dx, dy);
Log.d(TAG, "onViewPositionChanged: " + "left: " + left + " dx: " + dx);
int newLeft = left;
if(changedView == mLeftContent){
// 把当前变化量传递给mMainContent
newLeft = mMainContent.getLeft() + dx;
}
// 进行修正
newLeft = fixLeft(newLeft);
if(changedView == mLeftContent) {
// 当左面板移动之后, 再强制放回去.
mLeftContent.layout(0, 0, 0 + mWidth, 0 + mHeight);
mMainContent.layout(newLeft, 0, newLeft + mWidth, 0 + mHeight);
}
// 更新状态,执行动画
dispatchDragEvent(newLeft);
// 为了兼容低版本, 每次修改值之后, 进行重绘
invalidate();
}
// 4. 当View被释放的时候, 处理的事情(执行动画)
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// View releasedChild 被释放的子View
// float xvel 水平方向的速度, 向右为+
// float yvel 竖直方向的速度, 向下为+
Log.d(TAG, "onViewReleased: " + "xvel: " + xvel + " yvel: " + yvel);
super.onViewReleased(releasedChild, xvel, yvel);
// 判断执行 关闭/开启
// 先考虑所有开启的情况,剩下的就都是关闭的情况
if(xvel == 0 && mMainContent.getLeft() > mRange / 2.0f){
open();
}else if (xvel > 0) {
open();
}else {
close();
}
}
@Override
public void onViewDragStateChanged(int state) {
// TODO Auto-generated method stub
super.onViewDragStateChanged(state);
}
/**
* 根据范围修正左边值
* @param left
* @return
*/
private int fixLeft(int left) {
if(left < 0){
return 0;
}else if (left > mRange) {
return mRange;
}
return left;
}
在view移动的时候也可以用伴随动画:
private void animViews(float percent) {
// > 1. 左面板: 缩放动画, 平移动画, 透明度动画
// 缩放动画 0.0 -> 1.0 >>> 0.5f -> 1.0f >>> 0.5f * percent + 0.5f
// mLeftContent.setScaleX(0.5f + 0.5f * percent);
// mLeftContent.setScaleY(0.5f + 0.5f * percent);
ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));
ViewHelper.setScaleY(mLeftContent, 0.5f + 0.5f * percent);
// 平移动画: -mWidth / 2.0f -> 0.0f
ViewHelper.setTranslationX(mLeftContent, evaluate(percent, -mWidth / 2.0f, 0));
// 透明度: 0.5 -> 1.0f
ViewHelper.setAlpha(mLeftContent, evaluate(percent, 0.5f, 1.0f));
// > 2. 主面板: 缩放动画
// 1.0f -> 0.8f
ViewHelper.setScaleX(mMainContent, evaluate(percent, 1.0f, 0.8f));
ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));
// > 3. 背景动画: 亮度变化 (颜色变化)
getBackground().setColorFilter((Integer)evaluateColor(percent, Color.BLACK, Color.TRANSPARENT), Mode.SRC_OVER);