1、Android控件架构 2、View的测量与绘制 3、ViewGroup的测量与绘制 4、自定义控件的三种方式 5、事件的拦截机制
1、Android控件架构
1、控件分为2类:View控件和ViewGroup控件 2、上层控件负责下层控件的测量与绘制,并传递交互事件 3、每棵树的顶部都有一个ViewParent对象,这是控制核心,负责交互管理事件的调度和分配 4、每个Activity都包含一个Window对象,Window对象通常由PhoneWindow来实现 
DecorView是根View,它把内容显示在PhoneWindow上面, DecorView监听所有的事件并通过WindowManagerService来进行接收 DecorView通过Activity回调onClick事件 DecorView包含TitleView和ContentView ContentView是一个ID为content的FramLayout
DecorView的里面是一个垂直的LinearLayout,作为ViewGroup,上面title,下面content 所以requestWindowFeature(Window.FEATURE_NO_TITLE);只能在setContentView之前调用才能生效 (requestWindowFeature实际上是调用的PhoneWindow的requestFeature方法, 如果你把requestWindowFeature放在setContentView之后,我们来看 public boolean requestFeature(int featureId) { if (mContentParent != null) { throw new AndroidRuntimeException(“requestFeature() must be called before adding content”); } 那么mContentParent就有值了,就会抛出异常了) 5、在代码中,onCreate调用setContentView之后,ActivityManagerService会回调onResume方法,最终显示出来
2、View的测量与绘制
2.1、View的测量 1、Android就像那么蒙着眼睛画画的人,你必须精确的告诉它如何去画 2、我们在画一个图形之前,必须知道它的大小和位置,这就是测量 3、Android在onMeasure方法中进行测量,Android为我们提供了一个MeaSureSpec类来进行测量 4、MeaSureSpec是一个32位的int值,高2位为测量的模式,低30位为测量的大小 5、在计算中用位运算的原因是提高运算效率 6、测量模式有三种:
6.1、EXACTLY :精准值模式 当我们将控件设置为具体值时,比如android:layout_width = “100dp”或者为match_parent属性时,系统使用的是EXACTLY模式 6.2、AT MOST:最大值模式 当控件的width或者height属性为wrap_content,控件会随着内容的变化而变化,此时控件的尺寸只要不超过父控件指定的最大尺寸即可 6.3UNSPECIFIED:不指定测量模式,view想多大就多大,通常自定义view的时候使用 View默认的onMeasure方法只支持EXACTLY模式,所以自定义view的时候想要让控件支持warp_cotent属性,要重写onMeasure方法来指定warp_contetn的大小 7、onMeasure方法最终调用的是View的setMeasuredDimension方法,我们要做的就是把测量后的宽高设置给它 完整的代码如下:
package com.zx.heros.chapter3;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.util.AttributeSet;import android.util.Log;import android.view.View;/** * Created by Administrator on 2017/2/8. */public class TeachingView extends View { public TeachingView(Context context) { super(context); } public TeachingView(Context context, AttributeSet attrs) { super(context, attrs); } public TeachingView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override PRotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension( measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); } private int measureWidth(int measureSpec) { //从MeasureSpec提取出具体的测量模式和大小 int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); //如果是EXACTLY模式,直接使用指定的大小 //如果是其他模式,那么取我们指定值和specSize的最小值 if (specMode == MeasureSpec.EXACTLY) { result = specSize; } else { result = 200; if (specMode == MeasureSpec.AT_MOST) { result = Math.min(result, specSize); } } return result; } private int measureHeight(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY) { result = specSize; } else { result = 200; if (specMode == MeasureSpec.AT_MOST) { result = Math.min(result, specSize); } } return result; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(Color.GRAY); int width = getWidth(); int height = getHeight(); Log.d("xys", "width : " + width + " height : " + height); }}那么你在布局文件中使用这个view的时候:1、<com.zx.heros.chapter3.TeachingView android:layout_width="100dp" android:layout_height="100dp" />设置固定值,就是设置的大小2、<com.zx.heros.chapter3.TeachingView android:layout_width="wrap_content" android:layout_height="wrap_content" />设置wrap_content,则会使用你设置的默认大小200 这就是View的测量
2.2、View的绘制
1、测量一个view之后,我们就可以重写onDraw方法进行绘制了 2、在onDraw方法中,提供的有Canvas对象,我们可以直接用它进行绘制 3、其他地方想绘制可以通过Canvas can = new Canvas(bitmap);
3、ViewGroup的测量与绘制
3.1、ViewGroup的测量 1、ViewGroup会管理子View,当ViewGroup大小设置为wrap_cotent时,会根据子view的大小决定自己的大小,其他模式下(match_parent,固定值)根据指定值设置大小 2、子view测量完毕后,ViewGroup会通过调用子view的Layout方法决定布局位置 3、在自定义ViewGroup时会重写onLayout方法来控制子View显示的位置,如果需要支持wrap_content属性,那么也需要重写onMeasure方法给它一个默认result 3.2、ViewGroup的绘制 ViewGroup不需要绘制,但是它会调用dispatchDraw方法来绘制子view,过程是遍历子view,调用子view的绘制方法
4、自定义View
1、原生的view还存在不少bug,更不要提我们自定义的view了 2、了解系统自定义view的过程,帮助我们了解系统的绘图机制,适当情况下,可以通过自定义view创建灵活的布局 3、一般使用onDraw绘制view的显示内容,如果自定义view需要wrap_content属性,那就必须重写onMeasure方法了,通过自定义attr属性,可以自定义新的属性 自定义View几个重要的回调方法: onFinishInflate :从xml中加载组件后回调 onSizeChanged : 组件大小改变时回调 onMeasure :回调该方法来进行测量 onLayout :回调该方法来确定显示的位置 onTouchEvent : 监听到触摸事件时回调 自定义view时可以选择性回调方法 自定义view通常使用的方法: 1、对现有控件进行扩展 一般情况下,我们在onDraw方法中对原生控件进行扩展 比如:
package com.zx.heros.chapter3;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.util.AttributeSet;import android.widget.TextView;public class MyTextView extends TextView { private Paint mPaint1, mPaint2; public MyTextView(Context context) { super(context); initView(); } public MyTextView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } private void initView() { mPaint1 = new Paint(); mPaint1.setColor(getResources().getColor( android.R.color.holo_blue_light)); mPaint1.setStyle(Paint.Style.FILL); mPaint2 = new Paint(); mPaint2.setColor(Color.YELLOW); mPaint2.setStyle(Paint.Style.FILL); } @Override protected void onDraw(Canvas canvas) { // 绘制外层矩形 canvas.drawRect( 0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint1); // 绘制内层矩形 canvas.drawRect( 10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, mPaint2); canvas.save(); // 绘制文字前平移10像素 canvas.translate(10, 0); // 父类完成的方法,即绘制文本 super.onDraw(canvas); canvas.restore(); }}还有复杂一点的,可以通过LinearGradient Shader 和Matrix来实现,首先在onSizeChange方法中进行初始化工作,根据view的宽度,设置一个LinearGradient 渐变渲染器,然后在onDraw方法中通过矩阵的方式不断平移渐变效果 如下:
package com.zx.heros.chapter3;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.LinearGradient;import android.graphics.Matrix;import android.graphics.Paint;import android.graphics.Shader;import android.util.AttributeSet;import android.widget.TextView;public class ShineTextView extends TextView { private LinearGradient mLinearGradient; private Matrix mGradientMatrix; private Paint mPaint; private int mViewWidth = 0; private int mTranslate = 0; public ShineTextView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (mViewWidth == 0) { mViewWidth = getMeasuredWidth(); if (mViewWidth > 0) { mPaint = getPaint(); mLinearGradient = new LinearGradient( 0, 0, mViewWidth, 0, new int[]{ Color.BLUE, 0xffffffff, Color.BLUE}, null, Shader.TileMode.CLAMP); mPaint.setShader(mLinearGradient); mGradientMatrix = new Matrix(); } } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mGradientMatrix != null) { mTranslate += mViewWidth / 5; if (mTranslate > 2 * mViewWidth) { mTranslate = -mViewWidth; } mGradientMatrix.setTranslate(mTranslate, 0); mLinearGradient.setLocalMatrix(mGradientMatrix); postInvalidateDelayed(100); } }}2、通过组合来实现新的控件
2.1、定义属性,在res-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>然后在自定义view的构造方法中一一获取,获取到之后再设置给相应的组件(组件一个从xml里面引入,也可以new一个新的组件,然后设置宽高);这样的话,你在布局中使用这个自定义view的时候就可以使用attrs.xml里面的TopBar的属性了 下面是完整代码:
package com.imooc.systemwidget;import android.content.Context;import android.content.res.TypedArray;import android.graphics.drawable.Drawable;import android.util.AttributeSet;import android.view.Gravity;import android.view.View;import android.widget.Button;import android.widget.RelativeLayout;import android.widget.TextView;public class TopBar extends RelativeLayout { // 包含topbar上的元素:左按钮、右按钮、标题 private Button mLeftButton, mRightButton; private TextView mTitleView; // 布局属性,用来控制组件元素在ViewGroup中的位置 private LayoutParams mLeftParams, mTitlepParams, mRightParams; // 左按钮的属性值,即我们在atts.xml文件中定义的属性 private int mLeftTextColor; private Drawable mLeftBackground; private String mLeftText; // 右按钮的属性值,即我们在atts.xml文件中定义的属性 private int mRightTextColor; private Drawable mRightBackground; private String mRightText; // 标题的属性值,即我们在atts.xml文件中定义的属性 private float mTitleTextSize; private int mTitleTextColor; private String mTitle; // 映射传入的接口对象 private topbarClickListener mListener; public TopBar(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public TopBar(Context context) { super(context); } public TopBar(Context context, AttributeSet attrs) { super(context, attrs); // 设置topbar的背景 setBackgroundColor(0xFFF59563); // 通过这个方法,将你在atts.xml中定义的declare-styleable // 的所有属性的值存储到TypedArray中 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar); // 从TypedArray中取出对应的值来为要设置的属性赋值 mLeftTextColor = ta.getColor( R.styleable.TopBar_leftTextColor, 0); mLeftBackground = ta.getDrawable( R.styleable.TopBar_leftBackground); mLeftText = ta.getString(R.styleable.TopBar_leftText); mRightTextColor = ta.getColor( R.styleable.TopBar_rightTextColor, 0); mRightBackground = ta.getDrawable( R.styleable.TopBar_rightBackground); mRightText = ta.getString(R.styleable.TopBar_rightText); mTitleTextSize = ta.getDimension( R.styleable.TopBar_titleTextSize, 10); mTitleTextColor = ta.getColor( R.styleable.TopBar_titleTextColor, 0); mTitle = ta.getString(R.styleable.TopBar_title); // 获取完TypedArray的值后,一般要调用 // recyle方法来避免重新创建的时候的错误 ta.recycle(); mLeftButton = new Button(context); mRightButton = new Button(context); mTitleView = new TextView(context); // 为创建的组件元素赋值 // 值就来源于我们在引用的xml文件中给对应属性的赋值 mLeftButton.setTextColor(mLeftTextColor); mLeftButton.setBackground(mLeftBackground); mLeftButton.setText(mLeftText); mRightButton.setTextColor(mRightTextColor); mRightButton.setBackground(mRightBackground); mRightButton.setText(mRightText); mTitleView.setText(mTitle); mTitleView.setTextColor(mTitleTextColor); mTitleView.setTextSize(mTitleTextSize); mTitleView.setGravity(Gravity.CENTER); // 为组件元素设置相应的布局元素 mLeftParams = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE); // 添加到ViewGroup addView(mLeftButton, mLeftParams); mRightParams = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE); addView(mRightButton, mRightParams); mTitlepParams = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE); addView(mTitleView, mTitlepParams); // 按钮的点击事件,不需要具体的实现, // 只需调用接口的方法,回调的时候,会有具体的实现 mRightButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mListener.rightClick(); } }); mLeftButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mListener.leftClick(); } }); } // 暴露一个方法给调用者来注册接口回调 // 通过接口来获得回调者对接口方法的实现 public void setOnTopbarClickListener(topbarClickListener mListener) { this.mListener = mListener; } /** * 设置按钮的显示与否 通过id区分按钮,flag区分是否显示 * * @param id id * @param flag 是否显示 */ public void setButtonVisable(int id, boolean flag) { if (flag) { if (id == 0) { mLeftButton.setVisibility(View.VISIBLE); } else { mRightButton.setVisibility(View.VISIBLE); } } else { if (id == 0) { mLeftButton.setVisibility(View.GONE); } else { mRightButton.setVisibility(View.GONE); } } } // 接口对象,实现回调机制,在回调方法中 // 通过映射的接口对象调用接口中的方法 // 而不用去考虑如何实现,具体的实现由调用者去创建 public interface topbarClickListener { // 左按钮点击事件 void leftClick(); // 右按钮点击事件 void rightClick(); }}3、重写view来实现新的控件
3.1、难点在于绘制控件和实现交互,通常需要继承View类,并重写onDraw onMeasure方法实现绘制逻辑 3.2、我们来实现一个弧线比例展示图 
这个自定义View分为3个部分,中间的圆形,中间的文字,和外圈的弧线,有了这样的思路,在onDraw中一个一个的去绘制就可以了 1、初始化的时候设置好绘制3种图形的参数 初始化内圆半径和周长;绘制弧线,需要指定其椭圆的外接
package com.imooc.systemwidget;import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.Paint.Style;import android.graphics.RectF;import android.util.AttributeSet;import android.view.View;public class CircleProgressView extends View { private int mMeasureHeigth; private int mMeasureWidth; private Paint mCirclePaint; private float mCircleXY; private float mRadius; private Paint mArcPaint; private RectF mArcRectF; private float mSweepAngle; private float mSweepValue = 66; private Paint mTextPaint; private String mShowText; private float mShowTextSize; public CircleProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CircleProgressView(Context context, AttributeSet attrs) { super(context, attrs); } public CircleProgressView(Context context) { super(context); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mMeasureWidth = MeasureSpec.getSize(widthMeasureSpec); mMeasureHeigth = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(mMeasureWidth, mMeasureHeigth); initView(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制圆 canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint); // 绘制弧线 canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint); // 绘制文字 canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint); } private void initView() { //初始化的时候设置好三种图像的参数 float length = 0; if (mMeasureHeigth >= mMeasureWidth) { length = mMeasureWidth; } else { length = mMeasureHeigth; } mCircleXY = length / 2; mRadius = (float) (length * 0.5 / 2); mCirclePaint = new Paint(); mCirclePaint.setAntiAlias(true); mCirclePaint.setColor(getResources().getColor( android.R.color.holo_blue_bright)); mArcRectF = new RectF( (float) (length * 0.1), (float) (length * 0.1), (float) (length * 0.9), (float) (length * 0.9)); mSweepAngle = (mSweepValue / 100f) * 360f; mArcPaint = new Paint(); mArcPaint.setAntiAlias(true); mArcPaint.setColor(getResources().getColor( android.R.color.holo_blue_bright)); mArcPaint.setStrokeWidth((float) (length * 0.1)); mArcPaint.setStyle(Style.STROKE); mShowText = setShowText(); mShowTextSize = setShowTextSize(); mTextPaint = new Paint(); mTextPaint.setTextSize(mShowTextSize); mTextPaint.setTextAlign(Paint.Align.CENTER); } private float setShowTextSize() { this.invalidate(); return 50; } private String setShowText() { this.invalidate(); return "Android Skill"; } public void forceInvalidate() { this.invalidate(); } public void setSweepValue(float sweepValue) { if (sweepValue != 0) { mSweepValue = sweepValue; } else { mSweepValue = 25; } this.invalidate(); }}无论多么复杂的图形,控件,都是由最基本的图形绘制出来的,当你的脑海中有设计图之后,剩下的就是对坐标的计算了。 2、假如要实现这个图形
1、绘制一个一个的矩形 2、每个矩形之间稍微偏移一点距离即可 3、让这些矩形的高度进行随机的变化,通过Math.random() 4、在onDraw方法中调用invalidate方法通知view进行重绘,不过这里不需要每次一绘制完新的矩形就通知view进行重绘,这样会因为刷新速度太快影响效果,所以我们使用延迟重绘postinvalidateDelayed(300),每隔300秒进行一次重绘。 5、如果想更加逼真,可以在绘制矩形的时候给Paint增加一个LinearGradient渐变效果。在onSizeChange中增加。 完整代码如下:
这个例子告诉我们:创建自定义view要一步一步来,从基本效果开始慢慢增加功能,不论多么复杂的自定义view ,它一定是慢慢迭代起来的功能
5、自定义ViewGroup 1、自定义ViewGroup的目的是对子view进行管理,对子view添加显示、响应的规则 2、因此自定义ViewGroup需要重写onMeasure方法对子view进行测量;重写onLayout确定子view的位置,重写onTouchEvent增加响应事件 下面我们来自定义一个类似ScrollView来实现回弹的一个效果 1、让自定义ViewGroup先实现ScrollView的功能 2、实现黏性效果 具体代码如下:
package com.imooc.systemwidget;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;public class MyScrollView extends ViewGroup { private int mScreenHeight; private Scroller mScroller; private int mLastY; private int mStart; private int mEnd; public MyScrollView(Context context) { super(context); initView(context); } public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); initView(context); } public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context); } private void initView(Context context) { WindowManager wm = (WindowManager) context.getSystemService( Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(dm); mScreenHeight = dm.heightPixels; mScroller = new Scroller(context); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); // 2、设置ViewGroup的高度:即子View的个数乘以屏幕的高度 MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); mlp.height = mScreenHeight * childCount; setLayoutParams(mlp); //3、通过遍历来设定每个子View需要放置的位置,直接通过子view的layout方法, //并将具体的位置通过参数传递进去即可 for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() != View.GONE) { child.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight); } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //1、放置ViewGroup的子view,使用遍历的方式来通知子View进行测量 int count = getChildCount(); for (int i = 0; i < count; ++i) { View childView = getChildAt(i); measureChild(childView, widthMeasureSpec, heightMeasureSpec); } } @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: //4、只要使用scrollby(0,dy)方法,让手指滑动的时候,让ViewGroup的所有子view也跟着滚动dy即可 if (!mScroller.isFinished()) { mScroller.abortAnimation(); } int dy = mLastY - y; if (getScrollY() < 0) { dy = 0; } if (getScrollY() > getHeight() - mScreenHeight) { dy = 0; } scrollBy(0, dy); mLastY = y; break; case MotionEvent.ACTION_UP: //增加ViewGroup的黏性效果,手指离开后ViewGroup的黏性效果,很自然的想到onTouchEvent的ACTION_UP事件和Scroller //在ACTION_UP事件中判断手指滑动的距离,如果超过一定距离,则使用Scroller类来平滑移动到下一个view;如果 //小于一定距离,则回滚到原来的位置 //记录触摸终点 int dScrollY = checkAlignment(); if (dScrollY > 0) { if (dScrollY < mScreenHeight / 3) { mScroller.startScroll( 0, getScrollY(), 0, -dScrollY); } else { mScroller.startScroll( 0, getScrollY(), 0, mScreenHeight - dScrollY); } } else { if (-dScrollY < mScreenHeight / 3) { mScroller.startScroll( 0, getScrollY(), 0, -dScrollY); } else { mScroller.startScroll( 0, getScrollY(), 0, -mScreenHeight - dScrollY); } } break; } postInvalidate(); return true; } private int checkAlignment() { int mEnd = getScrollY(); boolean isUp = ((mEnd - mStart) > 0) ? true : false; int lastPrev = mEnd % mScreenHeight; int lastNext = mScreenHeight - lastPrev; if (isUp) { //向上的 return lastPrev; } else { return -lastNext; } } @Override public void computeScroll() { super.computeScroll(); if (mScroller.computeScrollOffset()) { scrollTo(0, mScroller.getCurrY()); postInvalidate(); } }}通过以上操作,我们就能在onTouchEvent中实现滚动的逻辑和黏性的逻辑
6、事件拦截机制分析
当Android系统捕获到用户的各种输入事件后,如何传递给需要这个事件的控件呢? 下面我们就来看看事件拦截机制分析 1、想要了解触摸事件的拦截机制,首先要了解什么事触摸事件? 比如按钮按下:按下一个事件,不小心滑动一点事件二,抬起事件三 2、Android为触摸事件封装了一个类MotionEvent,如果重写OnTouchEvent或者跟触摸相关的方法,它的参数就是MotionEvent。所以要牢记这个类 3、MotionEvent封装了不少好东西:1、触摸点的坐标event.getX()2、事件类型down、up、move 由此看来触摸事件还是比较简单的,动作类型+坐标而已 那么问题来了,viewGroup嵌套ViewGroup嵌套view,触摸事件就一个,该分配给谁呢?所以就有了事件拦截 举个栗子:
总经理:最外层的ViewGroup 部长:中间的ViewGroup 干活的你:MyView,最底层的view 1、总经理布置任务给部长 2、部长布置任务给你 3、你把任务完成交给了部长的任务,部长签字 4、部长交给总经理,总经理签字 这样一个任务就完成了,事件传递机制也是这样 对于ViewGroup来说: 重写:dispatchTouchEvent(派遣)、onInterceptTouchEvent(拦截)、onTouchEvent(触摸) 对于View来说: 重写:dispatchTouchEvent、onTouchEvent 可以看出ViewGroup级别比较高,比view多了一个方法:事件拦截的核心方法onInterceptTouchEvent
正常情况下: 事件传递的顺序是:总经理(外层ViewGroup)–>部长(中间ViewGroup)–>你(最底层的View) 事件传递的时候,先dispatchTouchEvent再onInterceptTouchEvent 事件处理的顺序是:你(最底层的View)–>部长(中间ViewGroup)–>总经理(外层ViewGroup)
事件处理的时候,都是执行onTouchEvent方法 事件传递的返回值很好理解:True:拦截,不继续 false不拦截,继续流程 事件处理的返回值也是:True:处理了,不用审核了,False:给上级处理 初始情况下返回值都是false 为了好理解事件拦截的过程,我们只关心onInterceptTouchEvent
下面我们继续上面的案例说明: 情况1、如果总经理觉得这个任务太简单了,自己可以完成,没必要找下属,那么事件就被总经理拦截了。 即:(外层ViewGroup的onInterceptTouchEvent返回true) 情况2、如果部长觉得没必要让下属解决,自己就解决了,那么事件就被部长拦截了。 即:(中间层的ViewGroup的onInterceptTouchEvent返回true)
对于事件的分发,拦截,大家都清楚了吧,下面我们来看看事件的处理 正常情况下你处理完事件要向上级汇报,需要上级确认,所以你就返回了false 情况1、如果你罢工不干了,那么你就不用报告上级了,就直接返回True 即:你(最底层的view)的onTouchEvent返回了true 情况2、你汇报的报告部长觉得太丢人,不敢给总经理看,就偷偷的返回的true,整个事件结束 即:部长(中间的viewGroup)的onTouchEvent返回了true
总结:从大到小派发事件,从小到大处理事件,初学者在学习的时候,最好先对流程有个大致认识之后,再去接触源码,这样就不会一头雾水,从而丧失学习的兴趣。
新闻热点
疑难解答