diff --git "a/Android/Fragment/006_Fragment344円270円216円ViewPager347円232円204円344円275円277円347円224円250円.md" "b/Android/Fragment/006_Fragment344円270円216円ViewPager347円232円204円344円275円277円347円224円250円.md" index 490b88f..3aaf0cf 100644 --- "a/Android/Fragment/006_Fragment344円270円216円ViewPager347円232円204円344円275円277円347円224円250円.md" +++ "b/Android/Fragment/006_Fragment344円270円216円ViewPager347円232円204円344円275円277円347円224円250円.md" @@ -16,59 +16,79 @@ FragmentPagerAdapter默认缓存两个页面,即当前显示页面的两边, - rDemoFragment 0-->setUserVisibleHint ==false - rDemoFragment 1-->setUserVisibleHint ==false - rDemoFragment 0-->onAttach - rDemoFragment 0-->onCreate - rDemoFragment 0-->setUserVisibleHint ==true - rDemoFragment 0-->onCreateView - rDemoFragment 0-->onViewCreated - rDemoFragment 0-->onActivityCreated - rDemoFragment 0-->onStart - rDemoFragment 0-->onResume - rDemoFragment 1-->onAttach - rDemoFragment 1-->onCreate - rDemoFragment 1-->onCreateView - rDemoFragment 1-->onViewCreated - rDemoFragment 1-->onActivityCreated - rDemoFragment 1-->onStart - rDemoFragment 1-->onResume + PagerOneFragment: PagerOneFragment-->onAttach + PagerOneFragment: PagerOneFragment-->onCreate + PagerOneFragment: PagerOneFragment-->setUserVisibleHint ==true + PagerOneFragment: onCreateView() + PagerOneFragment: PagerOneFragment-->onViewCreated + PagerOneFragment: PagerOneFragment-->onActivityCreated + PagerOneFragment: PagerOneFragment-->onStart + PagerOneFragment: PagerOneFragment-->onResume + + PagerTwoFragment: PagerTwoFragment-->onAttach + PagerTwoFragment: PagerTwoFragment-->onCreate + PagerTwoFragment: onCreateView() + PagerTwoFragment: PagerTwoFragment-->onViewCreated + PagerTwoFragment: PagerTwoFragment-->onActivityCreated + PagerTwoFragment: PagerTwoFragment-->onStart + PagerTwoFragment: PagerTwoFragment-->onResume 当滑动到第2个界面的时候,会初始化第三个界面的Fragment,加入到Activity中,此时设置第一个界面的Fragment的 `setUserVisibleHint ` 为flase,第二个界面的` setUserVisibleHint ` 为true - rDemoFragment 2-->setUserVisibleHint ==false - rDemoFragment 0-->setUserVisibleHint ==false - rDemoFragment 1-->setUserVisibleHint ==true - rDemoFragment 2-->onCreateView - rDemoFragment 2-->onViewCreated - rDemoFragment 2-->onActivityCreated - rDemoFragment 2-->onStart - rDemoFragment 2-->onResume + PagerOneFragment: PagerOneFragment-->setUserVisibleHint ==false + + PagerTwoFragment: PagerTwoFragment-->setUserVisibleHint ==true + + PagerThirdFragment: PagerThirdFragment-->onAttach + PagerThirdFragment: PagerThirdFragment-->onCreate + PagerThirdFragment: onCreateView() + PagerThirdFragment: PagerThirdFragment-->onViewCreated + PagerThirdFragment: PagerThirdFragment-->onActivityCreated + PagerThirdFragment: PagerThirdFragment-->onStart + PagerThirdFragment: PagerThirdFragment-->onResume 当滑动到第三个界面时,会销毁第一个界面的Fragment的视图,只保留其对象,设置第二个界面 `setUserVisibleHint ` 为flase,第三个界面的 `setUserVisibleHint` 为ture - rDemoFragment 1-->setUserVisibleHint ==false - rDemoFragment 2-->setUserVisibleHint ==true - rDemoFragment 0-->onAttach - rDemoFragment 0-->onStop - rDemoFragment 0-->onDestroyView + PagerTwoFragment: PagerTwoFragment-->setUserVisibleHint ==false + + PagerThirdFragment: PagerThirdFragment-->setUserVisibleHint ==true + + PagerOneFragment: PagerOneFragment-->onPause + PagerOneFragment: PagerOneFragment-->onStop + PagerOneFragment: PagerOneFragment-->onDestroyView + + PagerFourFragment: PagerFourFragment-->onAttach + PagerFourFragment: PagerFourFragment-->onCreate + PagerFourFragment: onCreateView() + PagerFourFragment: PagerFourFragment-->onViewCreated + PagerFourFragment: PagerFourFragment-->onActivityCreated + PagerFourFragment: PagerFourFragment-->onStart + PagerFourFragment: PagerFourFragment-->onResume 滑回第2个界面,重建第1个界面的视图: - rDemoFragment 0-->setUserVisibleHint ==false - rDemoFragment 2-->setUserVisibleHint ==false - rDemoFragment 1-->setUserVisibleHint ==true - rDemoFragment 0-->onCreateView - rDemoFragment 0-->onViewCreated - rDemoFragment 0-->onActivityCreated - rDemoFragment 0-->onStart - rDemoFragment 0-->onResume + PagerOneFragment: PagerOneFragment-->setUserVisibleHint ==false + + PagerThirdFragment: PagerThirdFragment-->setUserVisibleHint ==false + + PagerTwoFragment: PagerTwoFragment-->setUserVisibleHint ==true + + PagerOneFragment: onCreateView() + PagerOneFragment: PagerOneFragment-->onViewCreated + PagerOneFragment: PagerOneFragment-->onActivityCreated + + PagerFourFragment: PagerFourFragment-->onPause + PagerFourFragment: PagerFourFragment-->onStop + PagerFourFragment: PagerFourFragment-->onDestroyView + + PagerOneFragment: PagerOneFragment-->onStart + PagerOneFragment: PagerOneFragment-->onResume - 可以通过 mViewPager.setOffscreenPageLimit(int count);方法来设置viewPager缓存界面的个数,默认和最小值是1 ,表示缓存当前界面的两边各1个。 @@ -96,58 +116,81 @@ FragmemtStatePagerAdapter与FragmentPagerAdapter类似,同样实现getItem和g 生命周期如下: - rDemoFragment 0-->setUserVisibleHint ==false - rDemoFragment 1-->setUserVisibleHint ==false - rDemoFragment 0-->onAttach - rDemoFragment 0-->onCreate - rDemoFragment 0-->setUserVisibleHint ==true - rDemoFragment 0-->onCreateView - rDemoFragment 0-->onViewCreated - rDemoFragment 0-->onActivityCreated - rDemoFragment 0-->onStart - rDemoFragment 0-->onResume - rDemoFragment 1-->onAttach - rDemoFragment 1-->onCreate - rDemoFragment 1-->onCreateView - rDemoFragment 1-->onViewCreated - rDemoFragment 1-->onActivityCreated - rDemoFragment 1-->onStart - rDemoFragment 1-->onResume - - rDemoFragment 2-->setUserVisibleHint ==false - rDemoFragment 0-->setUserVisibleHint ==false - rDemoFragment 1-->setUserVisibleHint ==true - rDemoFragment 2-->onAttach - rDemoFragment 2-->onCreate - rDemoFragment 2-->onCreateView - rDemoFragment 2-->onViewCreated - rDemoFragment 2-->onActivityCreated - rDemoFragment 2-->onStart - rDemoFragment 2-->onResume - - rDemoFragment 0-->onSaveInstanceState - rDemoFragment 1-->setUserVisibleHint ==false - rDemoFragment 2-->setUserVisibleHint ==true - rDemoFragment 0-->onAttach - rDemoFragment 0-->onStop - rDemoFragment 0-->onDestroyView - rDemoFragment 0-->onDestroy - rDemoFragment 0-->onDetach - - - rDemoFragment 0-->setUserVisibleHint ==false - rDemoFragment 2-->setUserVisibleHint ==false - rDemoFragment 1-->setUserVisibleHint ==true - rDemoFragment 0-->onAttach - rDemoFragment 0-->onCreate - rDemoFragment 0-->onCreateView - rDemoFragment 0-->onViewCreated - rDemoFragment 0-->onActivityCreated - rDemoFragment 0-->onStart - rDemoFragment 0-->onResume - - - + //初始化 + PagerOneFragment: PagerOneFragment-->onAttach + PagerOneFragment: PagerOneFragment-->onCreate + PagerOneFragment: PagerOneFragment-->setUserVisibleHint ==true + PagerOneFragment: onCreateView() + PagerOneFragment: PagerOneFragment-->onViewCreated + PagerOneFragment: PagerOneFragment-->onActivityCreated + PagerOneFragment: PagerOneFragment-->onStart + PagerOneFragment: PagerOneFragment-->onResume + + PagerTwoFragment: PagerTwoFragment-->onAttach + PagerTwoFragment: PagerTwoFragment-->onCreate + PagerTwoFragment: onCreateView() + PagerTwoFragment: PagerTwoFragment-->onViewCreated + PagerTwoFragment: PagerTwoFragment-->onActivityCreated + PagerTwoFragment: PagerTwoFragment-->onStart + PagerTwoFragment: PagerTwoFragment-->onResume + + //第二个界面 + PagerOneFragment: PagerOneFragment-->setUserVisibleHint ==false + PagerTwoFragment: PagerTwoFragment-->setUserVisibleHint ==true + + PagerThirdFragment: PagerThirdFragment-->onAttach + PagerThirdFragment: PagerThirdFragment-->onCreate + PagerThirdFragment: onCreateView() + PagerThirdFragment: PagerThirdFragment-->onViewCreated + PagerThirdFragment: PagerThirdFragment-->onActivityCreated + PagerThirdFragment: PagerThirdFragment-->onStart + PagerThirdFragment: PagerThirdFragment-->onResume + + //第三个界面 + PagerTwoFragment: PagerTwoFragment-->setUserVisibleHint ==false + + PagerThirdFragment: PagerThirdFragment-->setUserVisibleHint ==true + + PagerOneFragment: PagerOneFragment-->onPause + PagerOneFragment: PagerOneFragment-->onStop + PagerOneFragment: PagerOneFragment-->onDestroyView + PagerOneFragment: PagerOneFragment-->onDestroy + PagerOneFragment: PagerOneFragment-->onDetach + + PagerFourFragment: PagerFourFragment-->onAttach + PagerFourFragment: PagerFourFragment-->onCreate + PagerFourFragment: mTextView.getParent():null + PagerFourFragment: onCreateView() + PagerFourFragment: PagerFourFragment-->onViewCreated + PagerFourFragment: PagerFourFragment-->onActivityCreated + PagerFourFragment: PagerFourFragment-->onStart + PagerFourFragment: PagerFourFragment-->onResume + + + //返回低二个界面 + PagerThirdFragment: PagerThirdFragment-->setUserVisibleHint ==false + + PagerTwoFragment: PagerTwoFragment-->setUserVisibleHint ==true + + PagerFourFragment: PagerFourFragment-->onPause + PagerFourFragment: PagerFourFragment-->onStop + PagerFourFragment: PagerFourFragment-->onDestroyView + PagerFourFragment: PagerFourFragment-->onDestroy + PagerFourFragment: PagerFourFragment-->onDetach + + PagerOneFragment: PagerOneFragment-->onAttach + PagerOneFragment: PagerOneFragment-->onCreate + PagerOneFragment: onCreateView() + PagerOneFragment: PagerOneFragment-->onViewCreated + PagerOneFragment: PagerOneFragment-->onActivityCreated + PagerOneFragment: PagerOneFragment-->onStart + PagerOneFragment: PagerOneFragment-->onResume + + + + + +需要注意的是,如果在ViewPager设置Adapter后就调用setCurrentItem(x)方法,则首先初始化的是指定位置的Fragment。 # 3 Adapter的notifyDataSetChanged @@ -166,3 +209,36 @@ FragmemtStatePagerAdapter与FragmentPagerAdapter类似,同样实现getItem和g in the adapter. 如果希望`otifyDataSetChanged`方法有效果,需要重写`getItemPosition`方法,然后返回`POSITION_NONE`。但是这种做法会让ViewPager移除所有的View,然后重新加载,代价还是有点高的。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/Android/UI/View344円275円223円347円263円273円/001342円200円224円342円200円224円View345円237円272円347円241円200円344円273円213円347円273円215円.md" "b/Android/UI/View344円275円223円347円263円273円/001342円200円224円342円200円224円View345円237円272円347円241円200円344円273円213円347円273円215円.md" new file mode 100644 index 0000000..c239102 --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/001342円200円224円342円200円224円View345円237円272円347円241円200円344円273円213円347円273円215円.md" @@ -0,0 +1,104 @@ +# 1 View介绍 + +View即代表视图,表示用户界面的基本模块,一个View占据了屏幕上的一个举行区域,并负责这个区域的绘制与事件处理。 +其次View是安卓系统所有UI控件的基类,而ViewGroup作为View的容器,其自身也是继承自View。 + +--- +

+ + +# 2 View相关知识介绍 + + +## 2.1 Android坐标系与视图坐标,View的位置参数 + + +在Android中屏幕的最左上角作为Android坐标系的**原点坐标**,而**视图坐标**是View的父节点的左上角坐标 + + View.getLocationInWindow()和 View.getLocationOnScreen()在window占据全部screen时, + 返回值相同,不同的典型情况是在Dialog中时。当Dialog出现在屏幕中间时, + View.getLocationOnScreen()取得的值要比View.getLocationInWindow()取得的值要大。 + +在View中: + +- x,y 表示view左上角在父View的坐标位置 +- translationX,translationY,View的左上角相对于父容器偏移量,默认都是0 +- top,bottom,left,right 看下图: + +![](img/001_view坐标.png) + + +其中translationX和translationY只能通过View自身的setTranslationX和setTranslationY改变,改变之后会影响x和y + +所以有一下公式: +x = left + translationX +y = top + translationY +right = left + getWidth(); +bottom = top + getHeight(); + + +## 2.2 与View相关的几个类 + +- MotionEvent 描述了一个事件 +- ViewConfiguration 系统关于View系统的一些配置,如TouchSlop +- VelocityTracker 速度跟踪器 +- GestureDetector 手势检测器 +- Scroller与OverScroller 滑动帮助类 +- ScaleGestureDetector 缩放手势帮助类 +- Canvas View的绘制 +- Paint/TextPaint View的绘制 +- Path View的绘制相关 + + + +这些类对于掌握View的相关知识,进行自定义View都非常重要,接下来会逐个学习 + + +


+ + + +# 2 自定义View + +在平时的开发中,体系提供的View可能无法满足界面需求,所以不得不进行自定义View,而自定义View涵盖的知识面非常广,但是也可以对其进行细致的分类,对于各个类别中都有其侧重的技术点,根据View的体系我们可以作如下**分类**: + +1. **继承View**,重写onDraw方法 + + 对于一些特殊的界面效果,无法使用已有的组件进行组合来达到这种效果,则可以考虑通过实现View的onDraw方法来实现自定义绘制。这种情况下需要对View的wrap_content属性进行处理,因为系统的View默认只支持match_parent。 + +2. **继承ViewGroup**,派生出特殊的layout + + 这种方式是需要实现特定的布局,而传统的布局无法满足界面需求,需要处理自身及子view的测量(onMeasure),和子view的布局(onLayout)。如果还有滑动相关的逻辑还需要处理好View的事件分发。 + +3. **继承特殊的View**,如TextView + + 对已有的View做功能加强或改变某些行为 + +4. **继承特定的ViewGroup**,如LinearLayout + +--- +## 2.1 自定义view需要掌握的知识 +对于自定义View除了上面提到的一些类之类,我们还需要熟悉下面列出的技能点: +- setContenLayout的处理流程 +- LayoutInflater inflate布局的流程 +- view的事件分发 +- view的绘制流程(遍历的过程):测量、布局、绘制 +- invalidate与postInvalidated的执行过程 +- 熟悉view的绘制技巧,最好有一定的数学知识 + + + + +## 2.2 自定义View须知 + +1. 让View支持wrap_content +2. 如果有需要,让view支持padding,让ViewGroup支持margin +3. 尽量不要再View中使用Handler,因为View本身就有post系列方法 +4. View中如果有线程或者动画,需要及时停止,参考onDetachedFromWindow + onDetachedFromWindow会在View被remove或者所在Activity退出时调用,与onAttachedToWindow对应 +5. View如果有滑动逻辑,处理好滑动冲突 +6. 如果可以使用代码完成的布局,尽量不要使用xml,因为代码布局比xml布局快很多 +7. 看看support包中是否有一些关于View的一些兼容操作,如果有就尽量使用support包中的方法。如v4包中的ViewCompat + +--- +

diff --git "a/Android/UI/View344円275円223円347円263円273円/002342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-measure.md" "b/Android/UI/View344円275円223円347円263円273円/002342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-measure.md" new file mode 100644 index 0000000..b1c9764 --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/002342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-measure.md" @@ -0,0 +1,543 @@ +# 1 View的绘制流程 + +一个view要显示在界面上,需要经历一个view树的遍历过程,这个过程又可以分为三个过程,分别是: + +`measure(测量) -> layout(布局) --> draw(绘制)` + +* 测量 确定一个View的大小 +* 布局 确定view在父节点上的位置 +* 绘制 绘制view 的内容 + +这个过程的启动时一个叫ViewRoot.java类中 performTraversals()函数发起的,子view也可以通过一些方法来请求重新遍历view树,但是在遍历过程view树时并不是所有的view都需要重新测量,布局和绘制,在view树的遍历过程中,系统会问view是否需要重新绘制,如果需要才会真的去绘制view。 +>View有一个内部标识 mPrivateFlags,用来记录view是否需要进行某些操作 + + + +流程图如下所示: + +![](img/002_遍历过程.jpg) + + +接下来具体分析View的各个流程 + + + +--- +



+ + + +# 2 测量(Measure) + +## 2.1 measure方法分析 +测量用来确定一个View的大小,在ViewRoot中的performTraversals()中,调用decorView的measure方法,measure方法接收两个参数, + +最初的两个参数在ViewRoot方法中产生: + + private static int getRootMeasureSpec(int windowSize, int rootDimension) { + int measureSpec; + switch (rootDimension) { + case ViewGroup.LayoutParams.MATCH_PARENT: + // Window不能调整其大小,强制使根视图大小与Window一致 + measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); + break; + + case ViewGroup.LayoutParams.WRAP_CONTENT: + // Window可以调整其大小,为根视图设置一个最大值 + measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); + break; + + default: + // Window想要一个确定的尺寸,强制将根视图的尺寸作为其尺寸 + measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); + break; + } + return measureSpec; + } + + + +由此可见一般情况下,初始值都是窗口的大小。 + + + + + + +接下来分析measure方法: + + public final void measure(int widthMeasureSpec, int heightMeasureSpec) { + //如果要求重新布局肯定要先测量 + if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || + //如果测量规格变了 + widthMeasureSpec != mOldWidthMeasureSpec || + heightMeasureSpec != mOldHeightMeasureSpec) { + // 需要重新绘制则把标识置为没有设置尺寸 + mPrivateFlags &= ~MEASURED_DIMENSION_SET; + // 这里调用onMeasure方法,来进行真正的测量 + onMeasure(widthMeasureSpec, heightMeasureSpec); + //如果onMeasure完毕没有设置测量尺寸,抛异常 + if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { + throw new IllegalStateException("onMeasure() did not set the" + + " measured dimension by calling" + + " setMeasuredDimension()"); + } + //恢复标识位 + mPrivateFlags |= LAYOUT_REQUIRED; + } + //记录测量后设置的测量规格 + mOldWidthMeasureSpec = widthMeasureSpec; + mOldHeightMeasureSpec = heightMeasureSpec; + } + + +流程很清晰measure方法是fianl的,子类无法重写,所以只能通过onMeasure方法来实现自己的测量逻辑,在onMeasure中必须调用**setMeasuredDimension**方法,否则抛出异常。 + +--- +
+ +## 2.2 系统默认的onMeasure流程 + +再来看一下onMeasure的默认实现: + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), + getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); + } + +看一下获取建议的最小宽度逻辑: + + protected int getSuggestedMinimumWidth() { + //mMinWidth默认是0 + int suggestedMinWidth = mMinWidth; + //如果设置了view的背景,那么根据mBGDrawable来获取一个大小 + //而Drawable是否有宽度不确定,比如ShapeDrawable没有原始宽高 + //BitmapDrawable有原始宽高(图片的宽高) + if (mBGDrawable != null) { + final int bgMinWidth = mBGDrawable.getMinimumWidth(); + //如果最小宽度小于Drawable的宽度,还是取drawable的宽度 + if (suggestedMinWidth < bgMinWidth) { + suggestedMinWidth = bgMinWidth; + } + } + return suggestedMinWidth; + } + +**因此如果View没有背景那么getSuggestedMinimumWidth返回0,有背景根据设置背景的不同而不同。** + +然后是getDefaultSize的逻辑 + + public static int getDefaultSize(int size, int measureSpec) { + int result = size;//记录传入的建议尺寸 + //获取测量规格与尺寸 + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + switch (specMode) { + //父view对子view的搞定没有要求,则使用建议的高度 + case MeasureSpec.UNSPECIFIED: + result = size; + break; + //如果是下面两种模式则使用测量规格传入的尺寸,而这个specSize就是父view传入的尺寸。 + case MeasureSpec.AT_MOST: + case MeasureSpec.EXACTLY: + result = specSize; + break; + } + return result; + } + + +最后规定onMeasure就是调用setMeasuredDimension来设置尺寸了 + + protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { + mMeasuredWidth = measuredWidth; + mMeasuredHeight = measuredHeight; + //恢复标志位 + mPrivateFlags |= MEASURED_DIMENSION_SET; + } + + + + +**只有调用setMeasuredDimension才能获取子view的测量高度(宽度)**,下面方法可以说明,也就是说只有view的measure方法执行完了,调用getMeasuredHeight(getMeasuredWidth)才有意义。 + + public final int getMeasuredHeight() { + return mMeasuredHeight & MEASURED_SIZE_MASK; + } + + +分析系统的onMeasure方法,可知默认的系统只支持EXACTLY模式,AT_MOST的处理和EXACTLY是一样的,自定义控件,肯定需要处理AT_MOST的测量模式。而UNSPECIFIED一般出现在可以滚动的view中。 + +--- +
+ + + + +
+--- + + +# 3 MeasureSpec +上面说到View的测量模式,测量规格等,这些东西都封装在MeasureSpec中,现在来学习一下这个类 + + MeasureSpec { + //MODE_SHIFT是位偏移数 + private static final int MODE_SHIFT = 30; + //模式遮罩 + private static final int MODE_MASK = 0x3 << MODE_SHIFT; + //三种模式 + public static final int UNSPECIFIED = 0 << MODE_SHIFT; + public static final int EXACTLY = 1 << MODE_SHIFT; + public static final int AT_MOST = 2 << MODE_SHIFT; + //获取模式 + public static int getMode(int measureSpec) { + return (measureSpec & MODE_MASK); + } + //获取尺寸 + public static int getSize(int measureSpec) { + return (measureSpec & ~MODE_MASK); + } + ...... + } + +代码还是很简单的: + +用一个int型变量来表示一个测量规格,我们知道int型有32位,而MODE_SHIFT=30,三种模式用二进制表示分别是: + +- UNSPECIFIED:0<< MODE_SHIFT; +- EXACTLY:01 << MODE_SHIFT; +- AT_MOST:10 << MODE_SHIFT; + +都向左边移动了30位,然后再根据getMode和getSize的计算逻辑分析,我们可以知道: +用一个int型的二进制形式来表示一个测量规格,前2位标识测量模式,后30位标识测量尺寸,那么这三种模式分别表示什么意思呢? + +- UNSPECIFIED:表示父容器不对子view的大小做任何要求 +- EXACTLY:表示父容器确定了子view的大小,就是测量规格中的尺寸 +- AT_MOST:表示父容器不能确定子view大小,但是要求子view的大小不能超过测量规格中指定的尺寸 + +这里我们要明白一个概念,屏幕是有大小的,而View可以说是没有大小限制的,View可以很大很大(比如地图view中的MapView),只是view通过屏幕展示出来收到屏幕大小的限制而已。 + + +--- +
+ + + + + +
+--- + +# 4 LayoutParams + +LayoutParams描述了View的大小,对其方式等信息,而每个ViewGroup都可以根据自身的layout特性来定制自己的LayoutParams,我们来看一下系统中的LayoutParams: + +![](img/002_layoutParams.png) + +可以看到确实是这样的,系统中的各中ViewGroup都有自己的LayoutParams实现,大部分都集成MarginLayoutParams。 +ViewGroup.LayoutParams是其他所有LayoutParams的父类,而MarginLayoutParams多了margin的特性,如果要考虑margin,就得继承MarginLayoutParams。 + + +每个处于ViewGroup的View必然有自己的LayoutParams对象,不管是从xml布局中layout的,还是用代码add进ViewGroup的,看一下LayoutParams的生成方式: + +在LayoutInfalater的inflate中: + + if (root != null) { + if (DEBUG) { + System.out.println("Creating params from root: " + + root); + } + // Create layout params that match root, if supplied + params = root.generateLayoutParams(attrs); + if (!attachToRoot) { + // Set the layout params for temp if we are not + // attaching. (If we are, we use addView, below) + temp.setLayoutParams(params); + } + } + + +在代码addView中: + + public void addView(View child, int index) { + if (child == null) { + throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); + } + LayoutParams params = child.getLayoutParams(); + if (params == null) { + params = generateDefaultLayoutParams(); + if (params == null) { + throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null"); + } + } + addView(child, index, params); + } + +可以看到,View的LayoutParams可能在inflate中根据xml指定的属性被构建并指定,然后即使没有,只要一个View被添加到ViewGroup中,那么必然调用添加它的ViewGroup的相关方法来生成它的LayoutParams,所以说View的LayoutParams取决于它的父容器,而且如果在把一个View添加到ViewGroup前指定了LayoutParams参数,而这个LayoutParams与它的父容器不匹配就会报错,因为ViewGroup会检查被添加View的LayoutParams: + +这个方法是`checkLayoutParams` + +比如LinearLayout的checkLayoutParams方法: + + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LinearLayout.LayoutParams; + } + + +所以在自定义ViewGroup的时候,如果我们需要实现自己的LayoutParams,那么最好重写相关的关于LayoutParams方法,而且在自定义ViewGroup时,如果我们需要考虑margin,可能会调用到下面一个方法`measureChildWithMargins`,有这么一段逻辑`final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();`,如果我们用到measureChildWithMargins这个方法,又没有重写相关方法,必然会报错。因为ViewGroup默认的现实是构造ViewGroup.LayoutParams. + + +如果自定义LayoutParams一般需要重写的方法有: + + 1:生成默认的布局参数 + + generateDefaultLayoutParams() + + + 2:生成布局参数 ,从属性配置中生成我们的布局参数 + + android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p){ + return new CustomLayoutParams(p); + } + + android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs){ + return new CustomLayoutParams(getContext() , attrs); + } + + 3:检查当前布局参数是否是我们定义的类型这在code声明布局参数时常常用到 + protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) { + return p instanceof CustomLayoutParams; + } + + + + +--- + +## 4.1 LayoutParams中指定的宽高与测量的关系 + +下面以宽度为例子: + +LayoutParams.width可以取值为: + +- MATCH_PARENT(FILL_PARENT) = -1 +- WRAP_CONTENT = -2 +- 具体的值 + +这些值都可以通过xml属性指定,或者在代码中指定 + +而在ViewGroup方法中提供了根据这些值来获取测量规格的方法: + + public static int getChildMeasureSpec(int spec, int padding, int childDimension) { + //测量规格中指定的模式和尺寸 + int specMode = MeasureSpec.getMode(spec); + int specSize = MeasureSpec.getSize(spec); + //测量规则尺寸减去padding不能小于0 + int size = Math.max(0, specSize - padding); + //用于保存结果的临时变量 + int resultSize = 0; + int resultMode = 0; + //这里对父容器自身的模式进行分类处理 + switch (specMode) { + //父容器的mode是精确的 + case MeasureSpec.EXACTLY: + //子view指定了确切的大小 + if (childDimension>= 0) { + //那么它的模式必然是EXACTLY的,它的尺寸就是它指定的尺寸 + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + //子view指定了它要匹配父容器的大小 + } else if (childDimension == LayoutParams.MATCH_PARENT) { + //而且父容器的模式是EXACTLY的,那么就给它父容器的大小, + //而且也确定了子view的模式就是精确的 + resultSize = size; + resultMode = MeasureSpec.EXACTLY; + //子view指定了它要包裹自己就好,具体结果如何它现在也不知道,那就是它自己处理咯 + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + //那么父容器就不能确定子view的处理逻辑了,只能让子view自己去处理 + //但是你的处理结果肯定不能超过的我父容器的大小,所以给你一个限制 + //所以子view的模式就是AT_MOST + resultSize = size; + resultMode = MeasureSpec.AT_MOST; + } + break; + + //父容器的mode是AT_MOST + case MeasureSpec.AT_MOST: + //只要子view指定它要精确的数据,他的尺寸就是他要的尺寸,模式就是EXACTLY + if (childDimension>= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + //子view指定了它要匹配父容器的大小 + //但是父容器也确定自己到底有多大,只能给他一个大小限制 + } else if (childDimension == LayoutParams.MATCH_PARENT) { + // Child wants to be our size, but our size is not fixed. + // Constrain child to not be bigger than us. + resultSize = size; + resultMode = MeasureSpec.AT_MOST; + //类似上面 + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + // Child wants to determine its own size. It can't be + // bigger than us. + resultSize = size; + resultMode = MeasureSpec.AT_MOST; + } + break; + //父容器的mode是UNSPECIFIED + case MeasureSpec.UNSPECIFIED: + //只要子view指定它要精确的数据,他的尺寸就是他要的尺寸,模式就是EXACTLY + if (childDimension>= 0) { + // Child wants a specific size... let him have it + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + + } + /* + sUseZeroUnspecifiedMeasureSpec是新版本加的 + 所以这里可以的处理结果是如果父容器的模式是UNSPECIFIED的 + 那么给你子view的尺寸是0,模式也是UNSPECIFIED + 也就是说你子view随便玩 + */ + else if (childDimension == LayoutParams.MATCH_PARENT) { + // Child wants to be our size... find out how big it should + // be + resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; + resultMode = MeasureSpec.UNSPECIFIED; + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + // Child wants to determine its own size.... find out how + // big it should be + resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; + resultMode = MeasureSpec.UNSPECIFIED; + } + break; + } + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); + } + + +经过上面的分析,我们可以得出结论:**View的测量规格由父容器和自身指定的宽高属性共同决定。** + +如下图所示: +![](img/002_测量规格.png) + + +--- +
+ +## 4.2 ViewGroup的测量职责 + +ViewGroup并没有重写onMeasure方法,但是分析系统的Layout,发现他们的ViewGroup方法中都有对子view进行测量,所以ViewGroup作为view的容器,不仅仅是来测量自己,还需要对子view进行测量 + + +接下来分析一下ViewGroup提供给我们的测量方法: + +##### measureChildren + +measureChildren用来测量所有的子view,如果不考虑margin等因素,可以很方便的对子view进行测量,内部调用的是measureChild + + protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { + final int size = mChildrenCount; + final View[] children = mChildren; + //遍历测量所有的子view + for (int i = 0; i < size; ++i) { + final View child = children[i]; + //只要不是GONE的子view都进行测量 + if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { + measureChild(child, widthMeasureSpec, heightMeasureSpec); + } + } + } + + +##### measureChild + +measureChild方法用来测量单个子view,但是不考虑子view的margin + + protected void measureChild(View child, int parentWidthMeasureSpec, + int parentHeightMeasureSpec) { + //获取子view的LayoutParams + final LayoutParams lp = child.getLayoutParams(); + //调用getChildMeasureSpec来获取子view的测量规格 + //这里考虑的是自身的padding + final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + mPaddingLeft + mPaddingRight, lp.width); + final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, + mPaddingTop + mPaddingBottom, lp.height); + //最后调用子view的measure方法 + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + +##### measureChildWithMargins考虑了子view的margin + + protected void measureChildWithMargins(View child, + int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + + widthUsed, lp.width); + final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, + mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + + heightUsed, lp.height); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + +##### resolveSizeAndState和resolveSize + +resolveSize是View内部的方法(resolveSizeAndState高版本添加的) +三个参数说明: +size 包裹内容时,自己算出的尺寸 +measureSpec view的测量规格 +childMeasuredState 测量状态 + +作用是根据安卓系统的测量规范,从wrap_content模式时计算的期望的尺寸和View自身的测量规则中得到一个最合理的尺寸 + + public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { + //获取测量模式和尺寸 + final int specMode = MeasureSpec.getMode(measureSpec); + final int specSize = MeasureSpec.getSize(measureSpec); + final int result; + switch (specMode) { + //如果是AT_MOST + case MeasureSpec.AT_MOST: + //需要的尺寸比规定的最大尺寸要大,最终还是取较小的值,但是给他一个标记, + //说父容器规定的尺寸没有满足我的需求,但是还是得听父容器的 + if (specSize < size) { + result = specSize | MEASURED_STATE_TOO_SMALL; + } else { + result = size; + } + break; + //精确模式,直接用specSize + case MeasureSpec.EXACTLY: + result = specSize; + break; + //没有规定,直接用view想要的尺寸 + case MeasureSpec.UNSPECIFIED: + default: + result = size; + } + return result | (childMeasuredState & MEASURED_STATE_MASK); + } + + +--- +
+ + +# 5 总结: + +关于measure的相关方法都已经理了一遍,我们大概可以理清View树的测量流程了: +![](img/002_测量流程.png) + + +- View作为单个的控制,只需要对自身进行测量,自身的职责就是在各种测量模式下合理的设置自身的宽高,对于自定义view而言,需要处理好wrap_content模式下的测量,因为系统只支持match_parent模式。 + +- ViewGroup作为一个View,但是同时它也是View的容器,不仅仅需要对自身进行测量,还需要对它内部的子view进行测量,当测量模式是EXACTLY它的尺寸是固定的,但是当它的测量模式是AT_MOST时,他需要根据子view的大小和自身的测量规格共同来决定自身的大小。 + +>说了这么多,到底如何进行测量呢?下篇见。 diff --git "a/Android/UI/View344円275円223円347円263円273円/003342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-onMeasure344円270円200円350円210円254円345円206円231円346円263円225円344円270円216円347円233円270円345円205円263円346円200円273円347円273円223円.md" "b/Android/UI/View344円275円223円347円263円273円/003342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-onMeasure344円270円200円350円210円254円345円206円231円346円263円225円344円270円216円347円233円270円345円205円263円346円200円273円347円273円223円.md" new file mode 100644 index 0000000..119140d --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/003342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-onMeasure344円270円200円350円210円254円345円206円231円346円263円225円344円270円216円347円233円270円345円205円263円346円200円273円347円273円223円.md" @@ -0,0 +1,405 @@ +# 1 View的onMeasure一般写法 + + private Bitmap mBitmap; + + private void init() { + mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.meinv); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // 获取宽度测量规格中的mode和size + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + // 声明一个临时变量来存储计算出的测量值 + int heightResult = 0; + int widthResult = 0; + + /* + * 如果父容器心里有数 + */ + if (widthMode == MeasureSpec.EXACTLY) { + // 那么子view就用父容器给定尺寸 + widthResult = widthSize; + } + /* + * 如果父容器不确定自身大小 + */ + else{ + // 那么子view可要自己看看自己需要多大了 + widthResult = mBitmap.getWidth()+getPaddingLeft()+getPaddingRight(); + /* + * 如果爹给儿子的是一个限制值 + */ + if (widthMode == MeasureSpec.AT_MOST) { + // 那么儿子自己的需求就要跟爹的限制比比看谁小要谁 + widthResult = Math.min(widthSize, widthResult); + } + } + + if (heightMode == MeasureSpec.EXACTLY) { + heightResult = widthSize; + }else{ + //考虑padding + heightResult = mBitmap.getHeight()+getPaddingBottom()+getPaddingTop(); + if (heightMode == MeasureSpec.AT_MOST) { + heightResult = Math.min(widthSize, heightResult); + } + } + // 设置测量尺寸 + setMeasuredDimension(widthResult, heightResult); + } + + + +流程就是: +1. 获取测量尺寸和模式,定义临时变量存储技术结果 + +2. 判断测量模式: + - 模式是EXACTL的,就使用测量规格中的尺寸 + - 模式是UNSPECIFIED,使用自身计算的尺寸 + - 模式是AT_MOST的,使用自身计算的尺寸与规定尺寸中较小的一个 + +3.设置测量尺寸 + + + + +##ViewGroup的onMeasure一般写法 + +由于ViewGroup的布局卡变万化,根本没有统一的模板,只能根据业务来定,一般大概流程就是: + +1. 首先ViewGroup作为容器,有测量所有子view的职责,所以第一步是遍历测量所有的字view。一般我们会选用ViewGroup提供的`measureChildWithMargins`方法,这个方法已经考虑了子view需要的margin和ViewGroup自身需要的padding。 +2. 遍历测量完所有子view,那么子view的宽高基本可以确定了,如果有特殊的需求,可以对子view进行多次测量。 +3. 根据自身的布局特性,来确定自身的宽高,但是需要遵守基本的测量规则 + + + +

+ + + +下面是在看爱哥博客是写的一个demo,仅供参考: + + + +```java + +/** + * 练习:实现一个自定义布局:里面所有的子view按照正方形的样式排列,允许定义多行多列 + */ +public class SquareEnhanceLayout extends ViewGroup { + private static final String TAG = SquareEnhanceLayout.class.getSimpleName(); + private int childMeasureState; + + public SquareEnhanceLayout(Context context) { + this(context, null); + } + + public SquareEnhanceLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SquareEnhanceLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ViewGroupMeasureDemoView); + mOrientation = array.getInt(R.styleable.ViewGroupMeasureDemoView_orientation, 0); + + mMaxColumn = array.getInteger(R.styleable.ViewGroupMeasureDemoView_column, 1); + mMaxRow = array.getInteger(R.styleable.ViewGroupMeasureDemoView_row, 1); + + Log.d("ViewGroupMeasureDemoVie", "orientation:" + mOrientation); + array.recycle(); + } + + private int mOrientation;//排列方向 + public final int ORIENTATION_VERTICAL = 0;//横向 + public final int ORIENTATION_HRIOZONTAL = 1;//纵向 + + public static final int DEFAULT_MAX_ROW = Integer.MAX_VALUE, DEFAULT_MAX_COLUMN = Integer.MAX_VALUE; + public int mMaxRow = DEFAULT_MAX_ROW;//最大行数 + public int mMaxColumn = DEFAULT_MAX_COLUMN;//最大列数 + + // + private void init() { +// 初始化最大行列数 + mMaxRow = mMaxColumn = 1; + } + + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // 声明临时变量存储父容器的期望值 + int parentDesireWidth = 0; + int parentDesireHeight = 0; + //获取子view的个数 + int count = getChildCount(); + + View child = null; + if (count> 0) {//有孩子采取测量 + + // 声明两个一维数组存储子元素宽高数据 + int[] childWidths = new int[getChildCount()]; + int[] childHeights = new int[getChildCount()]; + + + for (int i = 0; i < count; i++) { + child = getChildAt(i); + if (child.getVisibility() == View.GONE) {//如果view不可见,就不进行测量了 + + + continue; + } + + CustomLayoutParams customLayoutParams = (CustomLayoutParams) child.getLayoutParams(); + //对子view进行测量 并且考虑布局margin + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + //获取子view测量后的宽高指中的最大宽高值(这里需要子view都是按照正方形显示的) + int childMaxSize = Math.max(child.getMeasuredHeight(), child.getMeasuredWidth()); + //取最大值,让子view重新测量 + int childMeasureSpec = MeasureSpec.makeMeasureSpec(childMaxSize, MeasureSpec.EXACTLY); + child.measure(childMeasureSpec, childMeasureSpec); + + /* + * 考量外边距计算子元素实际宽高并将数据存入数组 + */ + childWidths[i] = child.getMeasuredWidth() + customLayoutParams.leftMargin + customLayoutParams.rightMargin; + childHeights[i] = child.getMeasuredHeight() + customLayoutParams.topMargin + customLayoutParams.bottomMargin; + + // 合并子元素的测量状态,跟着系统控件写即可 + childMeasureState = combineMeasuredStates(childMeasureState, child.getMeasuredState()); + + } + + + // 声明临时变量存储行/列宽高 + int indexMultiWidth = 0, indexMultiHeight = 0; + + //父view根据孩子的测量结果,来计算自己的大小 + if (mOrientation == ORIENTATION_HRIOZONTAL) {//横 + //如果子view的个数大于最大列数,说明需要换行 + if (count> mMaxColumn) { + + int row = count / mMaxColumn;//行数 + int remainder = count & mMaxColumn;//余数 + // 声明临时变量存储子元素宽高数组下标值 + int index = 0; + + for (int x = 0; x < row; x++) { + + for (int y = 0; y < mMaxColumn; y++) {//为什么是 x 0) { + for (int i = count - remainder; i < count; i++) { + indexMultiHeight = Math.max(indexMultiHeight, childHeights[i]); + indexMultiWidth += childWidths[i]; + } + //最后一行遍历完毕后 高度累加 宽度取最大值 + parentDesireHeight += indexMultiHeight; + parentDesireWidth = Math.max(parentDesireWidth, indexMultiWidth); + //一行遍历完毕 重置 + indexMultiHeight = 0; + indexMultiWidth = 0; + } + + } else { + //没有列数限制,就是一行 + for (int x = 0; x < count; x++) { + //横向的布局 横向累加,要考虑子view的margin + parentDesireWidth += childWidths[x]; + parentDesireHeight = Math.max(parentDesireHeight, childHeights[x]); + } + } + + } else if (mOrientation == ORIENTATION_VERTICAL) {//纵向 + + + } + + + //最后要考虑自身的padding + parentDesireWidth += getPaddingLeft() + getPaddingRight(); + parentDesireHeight += getPaddingTop() + getPaddingBottom(); + /* + * 尝试比较父容器期望值与Android建议的最小值大小并取较大值 + */ + parentDesireWidth = Math.max(getSuggestedMinimumWidth(), parentDesireWidth); + parentDesireHeight = Math.max(getSuggestedMinimumHeight(), parentDesireHeight); + + } + + /* + 这个resolveSize是View提供用来获取最合理size的一个工具方法 + + 具体实现在API 11由resolveSizeAndState处理,这个方法多了一个childMeasuredState参数,而上面例子我们在具体测量时也引入了一个childMeasureState临时变量的计算,那么这个值的作用是什么呢?说到这里不得不提API 11后引入的几个标识位: + MEASURED_HEIGHTSTATE_SHIFT 测量高度状态遮罩 + MEASURED_SIZE_MASK 测量尺寸遮罩 + MEASURED_STATE_MASK 测量状态遮罩 + MEASURED_STATE_TOO_SMALL 表示规定的size小于期望的size + + childMeasuredState这个值由View.getMeasuredState()这个方法返回,一个布局通过View.combineMeasuredStates()这个方法来统计其子元素的测量状态。在大多数情况下你可以简单地只传递0作为参数值,而子元素状态值目前的作用只是用来告诉父容器在对其进行测量得出的测量值比它自身想要的尺寸要小,如果有必要的话一个对话框将会根据这个原因来重新校正它的尺寸。 + 这里我们还是就按照谷歌官方的建议依葫芦画瓢。 + + 不过在看一些系统控件的写法时,很多操作(比如combineMeasuredStates)都使用了ViewCompat中的方法代替系统SDK提供的方法,已达到最好的兼容性,所以还是建议使用ViewCompat中的方法。 + */ + setMeasuredDimension(resolveSizeAndState(parentDesireWidth, widthMeasureSpec, childMeasureState), + resolveSizeAndState(parentDesireHeight, heightMeasureSpec, childMeasureState << MEASURED_HEIGHT_STATE_SHIFT)); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (changed) { + Log.d(TAG, String.format(" int l = %d, int t = %d, int = %d, int b = %d", l, t, r, b)); + + int count = getChildCount(); + int indexPoint = 1;//标识到了第几行或第几列 + int indexWidth = 0;//存储临时行的宽度 + int indexHeight = 0;//存储列的高度 + // 声明临时变量存储行/列临时宽高 + int tempHeight = 0, tempWidth = 0; + + CustomLayoutParams clp = null; + View child = null; + + for (int i = 0; i < count; i++) { + child = getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + clp = (CustomLayoutParams) child.getLayoutParams(); + if (mOrientation == ORIENTATION_HRIOZONTAL) { + + if (count> mMaxColumn) {//多行 + if (i < mMaxColumn * indexPoint) { + child.layout( + getPaddingLeft() + indexWidth + clp.leftMargin, + getPaddingTop() + indexHeight + clp.topMargin, + getPaddingLeft() + indexWidth + clp.leftMargin + child.getMeasuredWidth(), + getPaddingTop() + indexHeight + clp.topMargin + child.getMeasuredHeight() + ); + tempHeight = Math.max(tempHeight, clp.topMargin + clp.bottomMargin + child.getMeasuredHeight()); + indexWidth += child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin; + if (i + 1>= mMaxColumn * indexPoint) { + indexWidth = 0; + indexHeight += tempHeight; + indexPoint++; + } + + } + + + } else {//单行 + + child.layout( + getPaddingLeft() + indexWidth + clp.leftMargin, + getPaddingTop() + clp.topMargin, + getPaddingLeft() + indexWidth + clp.leftMargin + child.getMeasuredWidth(), + getPaddingTop() + clp.topMargin + child.getMeasuredHeight() + ); + indexWidth += child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin; + } + + } else if (mOrientation == ORIENTATION_VERTICAL) { + + } + } + + + } + } + + /** + * 一致地返回false,其作用是告诉framework我们当前的布局不是一个滚动的布局 + * @return + */ + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + + /* + * 实现跟LayoutParams相关的方法 + */ + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new CustomLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + + @Override + protected LayoutParams generateLayoutParams(LayoutParams p) { + return new CustomLayoutParams(p); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new CustomLayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(LayoutParams p) { + return p instanceof CustomLayoutParams; + } + + /** + * 根据需求,自定义一个LayoutParams + */ + public static class CustomLayoutParams extends MarginLayoutParams { + + public CustomLayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public CustomLayoutParams(int width, int height) { + super(width, height); + } + + public CustomLayoutParams(MarginLayoutParams source) { + super(source); + } + + public CustomLayoutParams(LayoutParams source) { + super(source); + } + } +} + +``` + + +# 2 其他总结 + +1,在自定义控件时,如果控件事不需要滑动的,可以重写`shouldDelayChildPressedState`方法,告诉Framework控件不是不滚动的 + +2,Gravity的使用 + + 在自定义属性时:直接使用android:layout_gravity这样的name而无需定义类型值, + 这样则表示我们的属性使用的Android自带的标签,之后我们只需根据布局文件中layout_gravity属性的值调用Gravity类下的方法去计算对齐方式则可, + Gravity类下的方法很好用,为其可以说是无关布局的,拿最简单的一个来说: + public static void apply(int gravity, int w, int h, Rect container, Rect outRect) 更多方法查看:http://www.programgo.com/article/42992498417/ + gravity 所需放置的对象,由该类中的常量定义 + w 对象的水平尺寸 + h 对象的垂直尺寸 + container 容器空间的框架,将用来放置指定对象,应该足够大,以包含对象的宽和高。 + outRect 接收对象在其容器中的计算帧(computed frame) + +掌握这些技巧对自定义控件会有很大的帮助。 diff --git "a/Android/UI/View344円275円223円347円263円273円/004342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-layout.md" "b/Android/UI/View344円275円223円347円263円273円/004342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-layout.md" new file mode 100644 index 0000000..d45261e --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/004342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-layout.md" @@ -0,0 +1,248 @@ +# 1 Layout流程 + +上篇笔记已经对view树的遍历和measure进行了讲解,但是我们也知道,view需要显示出来,需要进行三大步骤,这第二大不步骤就是layout了,layout用来确定子view的位置。 + + +layout方法也是从ViewRoot中发起的,2.3源码如下: + + host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight); + +在遍历测量完毕之后,就是执行遍历布局了 + +![](img/04_layout流程.png) + + +--- +

+ +# 2 Layout源码分析 + +**来看一下layout的源码** + +Layout方法中接受四个参数,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置时通常会根据子View在measure中测量的大小来决定。 + + + public final void layout(int l, int t, int r, int b) { + //通过setFrame判断是否参数改变了 + boolean changed = setFrame(l, t, r, b); + //如果改变了,或者mPrivateFlags被修改为必须进行重新布局则进行布局 + if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) { + if (ViewDebug.TRACE_HIERARCHY) { + ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT); + } + //重新布局 + onLayout(changed, l, t, r, b); + //重新恢复标志 + mPrivateFlags &= ~LAYOUT_REQUIRED; + } + mPrivateFlags &= ~FORCE_LAYOUT; + } + + +**setFrame方法源码** + +1. setFrame方法是一个隐藏方法,所以作为应用层程序员来说,无法重写该方法。该方法体内部通过比对本次的l、t、r、b四个值与上次是否相同来判断自身的位置和大小是否发生了改变。 +2. 如果发生了改变,将会调用invalidate请求重绘。 +3. 记录本次的l、t、r、b,用于下次比对。 +4. 如果大小发生了变化,onSizeChanged方法,该方法在大多数View中都是空实现,程序员可以重写该方法用于监听View大小发生变化的事件,在可以滚动的视图中重载了该方法,用于重新根据大小计算出需要滚动的值,以便显示之前显示的区域。 + + +```java + protected boolean setFrame(int left, int top, int right, int bottom) { + //一个临时变量,记录范围是否发送变化 + boolean changed = false; + ...... + //分别对l.t.r.b进行比较,只要有一个变化了,就说明范围发送变化 + if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { + changed = true; + // Remember our drawn bit + int drawn = mPrivateFlags & DRAWN; + // Invalidate our old position + invalidate(); + //计算尺寸 + int oldWidth = mRight - mLeft; + int oldHeight = mBottom - mTop; + //记录新的尺寸 + mLeft = left; + mTop = top; + mRight = right; + mBottom = bottom; + + mPrivateFlags |= HAS_BOUNDS; + + int newWidth = right - left; + int newHeight = bottom - top; + //如果view的尺寸变化了会回调onSizeChanged + if (newWidth != oldWidth || newHeight != oldHeight) { + onSizeChanged(newWidth, newHeight, oldWidth, oldHeight); + } + //如果view是可见的,需要进行重绘 + if ((mViewFlags & VISIBILITY_MASK) == VISIBLE) { + // If we are visible, force the DRAWN bit to on so that + // this invalidate will go through (at least to our parent). + // This is because someone may have invalidated this view + // before this call to setFrame came in, therby clearing + // the DRAWN bit. + mPrivateFlags |= DRAWN; + invalidate(); + } + + // Reset drawn bit to original value (invalidate turns it off) + mPrivateFlags |= drawn; + mBackgroundSizeChanged = true; + } + //返回结果 + return changed; + } +``` + +### layout后可以确定的变量 + +在调用了View的layout方法后,也就确定了View的位置了,View的最终宽高度也就确定了,为什么是最终宽高度呢?那测量的宽高又算什么呢?,这里需要了解的是,测量只是来计算View的需要的宽高度,用来给layout做参考,而最终确定View的位置和宽高的方法却是layout, + +如下面只有在layout之后,mTop,mBottom,mRight,mLeft才能确定 + + public final int getTop() { + return mTop; + } + + public final int getBottom() { + return mBottom; + } + + public final int getLeft() { + return mLeft; + } + + public final int getRight() { + return mRight; + } + +在看一下获取View的宽高: + + public final int getHeight() { + return mBottom - mTop; + } + + public final int getWidth() { + return mRight - mLeft; + } + +很明显就是一个减法计算,可以看到View的实际宽高确实是在layout后才确定的,那么`getHeight`,`getWidth`和`getMeasuredWidth`,`getMeasuredHeight`又有什么区别呢? + +看一下源码: + + public final int getMeasuredWidth() { + return mMeasuredWidth; + } + public final int getMeasuredHeight() { + return mMeasuredHeight; + } +返回的是设置的测量宽高,也就是说如果ViewGroup的layout流程遵循在测量中设置的测量尺寸,那么`getHeight`,`getWidth`和`getMeasuredWidth`,`getMeasuredHeight`没有任何区别,返回值必然是一样的,否则不一样! + +--- +

+ + +# 3 ViewGroup如何进行layout +ViewGroup的onLayout是一个抽象方法,也就是说我们如果自定义ViewGroup必然要重写此方法,具体如何进行layout要根据界面需求,根本没有统一的标准 + +只是在layout过程中,一定要考虑好子view的margin和自身的padding + +--- +

+ +# 4 如何在Actiivty启动的时候准确的获取View的宽高 + +之前的源码分析中我们知道,View树真正遍历的执行实在ActivityThread的handleResumeActivity方法的最后, + + final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) { + + ...... + //这里回调onStart , onResume + ActivityClientRecord r = performResumeActivity(token, clearHide); + ...... + if (r.activity.mVisibleFromClient) { + //View树构建完毕,真正开始遍历 + r.activity.makeVisible(); + } + + } + +也就是说我们在onCreate,onStart,onRsume中调用View的`getHeight`,`getWidth`和`getMeasuredWidth`,`getMeasuredHeight`返回的都会是0 + + +那么有什么办法呢? + +#### 方法1:Activity的onWindowFocusChanged + +Activity的onWindowFocusChanged表示View已经初始化完毕了,宽高已经准备好了,需要注意的是Activity的onWindowFocusChanged会被调用多次,当Activity的窗口得到焦点和失去焦点均会被调用,具体的说就是当Activity继续执行和暂停时Activity的onWindowFocusChanged都会执行,即onMeasure和onPause,代码如下: + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if(hasFocus){ + int viewHeight = mPasswordEdit.getWidth(); + int viewWidth = mPasswordEdit.getHeight(); + } + } + + +#### 方法2:View.post(Runable) + +通过post可以把一个消息添加到消息队列的尾部,这时候View肯定也就初始化好了 + + public void onCreate(Bundle icicle) { + super.onCreae(icicle) + + mPasswordEdit.post(new Runnable() { + @Override + public void run() { + int viewHeight = mPasswordEdit.getWidth(); + int viewWidth = mPasswordEdit.getHeight(); + } + }); + } + +#### 方法3:ViewTreeObserver +使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生变化或者VIew树内部的View可见性发生变化时,onGlobalLayout方法将会被回调,需要注意的是伴随着View的状态变化等,onGlobalLayout会被调用多次: + + mMultiStateView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") + @Override + public void onGlobalLayout() { + if(Build.VERSION.SDK_INT>= 16){ + mMultiStateView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + }else{ + mMultiStateView.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + //获取宽高 + } + }); + + + + + +#### 4:View.measure(w , h) + +通过手动调用view的measure方法来得到宽高,这种方法比较复杂,需要分类讨论: + +根据View的LayoutParams的宽高值来分: + +**1,MATCH_PARENT** + +直接放弃,因为父容器的宽高也不确定 + +**2,具体数值(dp/px)** + +直接获取即可 + +**3,WRAW_CONTENT** + + int size = 1 << 30 -1;//即后30位 + int measureSpcec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); + mMultiStateView.measure( + measureSpcec + , measureSpcec); diff --git "a/Android/UI/View344円275円223円347円263円273円/005342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-draw.md" "b/Android/UI/View344円275円223円347円263円273円/005342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-draw.md" new file mode 100644 index 0000000..4a592e6 --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/005342円200円224円342円200円224円View347円273円230円345円210円266円346円265円201円347円250円213円-draw.md" @@ -0,0 +1,181 @@ +# 1 Draw流程 + + +绘制是View树遍历流程的最后一个,前面说的测量和布局只是确定View的大小和位置,如果不对view进行绘制,那么界面上依然不会有任何图形显示出来,draw也是从ViewRoot中的performTraversals发起的。然后会view的draw相关方法,但是并不是每个View都需要执行绘制,在执行绘制的过程中,只会重绘需要绘制的View。 + +draw方法的流程为: + + ViewRoot调用DecorView的draw方法:ViewRoot-->DecorView.draw(canvas) + + DecorView的draw方法调用自己的dispatchDraw(Canvas canvas)方法,然后在此方法中会调用子view的 + draw(Canvas canvas, ViewGroup parent, long drawtime)方法,此方法会调用单个参数的draw(Canvas canvas)方法。 + + + +# 2 draw方法 + +view有两个重载的draw方法,分别是: + + draw(Canvas canvas, ViewGroup parent, long drawtime) + draw(Canvas canvas) + +draw(Canvas canvas, ViewGroup parent, long drawtime)方法由父view调用,此方法比较重要的,在这里会判断View的一些内部标识,还会对canvas做一些调整,如绘制区域与绘图坐标系的调整,不一定会调用view的 draw(Canvas canvas)方法,如果不调用则绘制的是view的缓存。具体可以查看相关方法的源码。 + + + + + + + + public void draw(Canvas canvas) { + ...... + //通过内部标识,判断View的行为 + final int privateFlags = mPrivateFlags; + final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE && + (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); + mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN; + + /* + *draw的步骤 + * + * 1. 画背景 + * 2. 如果需要, 为显示渐变框做一些准备操作 + * 3. 画内容(onDraw) + * 4. 画子view + * 5. 如果需要, 画一些渐变效果 + * 6. 画装饰内容,如滚动条 + */ + + // Step 1, draw the background, if needed + int saveCount; + + if (!dirtyOpaque) { + final Drawable background = mBGDrawable; + if (background != null) { + final int scrollX = mScrollX; + final int scrollY = mScrollY; + + if (mBackgroundSizeChanged) { + background.setBounds(0, 0, mRight - mLeft, mBottom - mTop); + mBackgroundSizeChanged = false; + } + + if ((scrollX | scrollY) == 0) { + background.draw(canvas); + } else { + canvas.translate(scrollX, scrollY); + background.draw(canvas); + canvas.translate(-scrollX, -scrollY); + } + } + } + + // skip step 2 & 5 if possible (common case) + final int viewFlags = mViewFlags; + boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; + boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; + //如果条件不成立,跳过2-5步 + if (!verticalEdges && !horizontalEdges) { + // Step 3,画内容 + if (!dirtyOpaque) onDraw(canvas); + + // Step 4,画孩子 + dispatchDraw(canvas); + + // Step 6, 画装饰(滚动条) + onDrawScrollBars(canvas); + + // we're done... + return; + } + + ...... + + // Step 6, draw decorations (scrollbars) + onDrawScrollBars(canvas); + } + + +**dispatchDraw&drawChild** + + protected void dispatchDraw(Canvas canvas) { + ...... + + for (int i = 0; i < count; i++) { + final View child = children[i]; + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { + more |= drawChild(canvas, child, drawingTime); + } + } + + + ...... + } + + + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + return child.draw(canvas, this, drawingTime); + } + + + + + +可以看到draw的过程分为: +1. 如果设置了,画背景 +2. 如果需要, 为显示渐变框做一些准备操作 +3. 调用onDraw画内容 +4. 调用dispatchDraw画子view +5. 如果需要, 画渐变框 +6. 画装饰内容,如前景与滚动条 + + + + +- onDraw是每个view需要实现的,否则View默认只能显示背景,而实现onDraw就是为了画出View的内容,而ViewGroup一般不需要实现onDraw,因为它仅仅是作为View的容器没有需要绘制东西, +- dispatchDraw用来遍历ViewGrop的所有子view,执行draw方法 + + + +# 3 onDraw中如何绘制 + +在系统源码中onDraw是个空实现方法,仅仅提供了一个Canvas画板,到底如何来画View的内容呢? + +如果需要熟练的绘制出各种效果的View,我们需要掌握很多知识: + +- Canvas的是使用 绘制-变化-图层操作等等 +- Paint 画笔 +- Path 路径 +- Bitmap Canvas是画布,但是我们需要画纸,Bitmap就是画纸 +- ColorMatrix和Matrix的熟练运用 +- PathMeasure + + + + + + +# 4 View的缓存优化 + +在Android的显示机制中,View的软件渲染都是基于bitmap图片进行的处理。并且刷新机制中只要是与脏数据区有交集的视图都将重绘,所以在View的设计中就有一个cache的概念存在,这个cache无疑就是一个bitmap对象。也就是说在绘制流程中View不一定会被重新绘制,有可能绘制的只是View的缓存。 + + + + + + + + + + + + + + + + + + + + + diff --git "a/Android/UI/View344円275円223円347円263円273円/006342円200円224円342円200円224円View347円232円204円344円272円213円344円273円266円345円210円206円345円217円221円346円272円220円347円240円201円345円210円206円346円236円220円(2.3).md" "b/Android/UI/View344円275円223円347円263円273円/006342円200円224円342円200円224円View347円232円204円344円272円213円344円273円266円345円210円206円345円217円221円346円272円220円347円240円201円345円210円206円346円236円220円(2.3).md" new file mode 100644 index 0000000..b9d27bf --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/006342円200円224円342円200円224円View347円232円204円344円272円213円344円273円266円345円210円206円345円217円221円346円272円220円347円240円201円345円210円206円346円236円220円(2.3).md" @@ -0,0 +1,265 @@ +# 1 事件分发 +View的事件分发在view知识体系中是很重要的,view与用户的交互都是通过事件分发进行的,掌握好view的事件处理是很有必要的, + +而在View体系中的事件处理中,主要与view和viewGroup有关,下面通过分析源码来学习view的事件分发. + + +>注意这里分析的是2.3版本的源码,因为2.3的源码分析起来更加清晰,而android之后对事件分发的相关代码改动比较大,不过怎么改基本的行为是不会改变的,只是改变一种实现方式而已。搞懂2.3版本的源码对理解较高版本的源码还是有帮助的。所以这篇主要专注于通过源码理解事件分发,不会注意太多的细节。 + +在Android中使用MotionEvent来表示一个事件,当事件产生后,WMS会通过IPC把事件分发给当前处于活动的窗口,在应用层,最先获取事件的是ViewRoot中的W类,然后通过ViewRoot把事件传递给它内部的mView,然后是Activity。但是对于我们程序员而已,能开始操作事件的起始位置是Activity。所以姑且把Activity作为事件分发的起点。 + +# 2 Activity对事件的处理 + +事件优先传递给Activity的dispatchTouchEvent方法, + + 来看一下Activity 的dispatchTouchEvent 方法: + public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + onUserInteraction(); + } + if (getWindow().superDispatchTouchEvent(ev)) { + return true; + } + return onTouchEvent(ev); + } + +onUserInteraction是个空方法,所以不用管,然后看 +getWindow().superDispatchTouchEvent(ev), +getWindow获取的是Window的实现类PhoneWindow, PhoneWindow的dispatchTouchEvent方法如下: + + public boolean superDispatchTouchEvent(MotionEvent event) { + return mDecor.superDispatchTouchEvent(event); + } +mDecor之前已经分析过了,就是View树的根view。之后事件就会分发到view中。 + + +# 3 关于View的事件分发 + + +当事件传递到view时,它的dispatchTouchEvent方法被调用 + +看View.dispatchTouchEvent 源码: + + +主要是三个方法: + +1. **dispatchTouchEvent** + +2. **onTouch ** + +3. **onTouchEvent ** + +事件会最先传递到dispatchTouchEvent 方法 +看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); + } + +这里会判断当前View是否可用,如果可用并且mOnTouchListener不为空就会调用mOnTouchListener .onTouch方法如果onTouch方法返回ture 就不会执行onTouchEvent方法,可见mOnTouchListener是View提供给外界优先处理事件的接口,接下来看一下 View.onTouchEvent 方法: + + public boolean onTouchEvent(MotionEvent event) { + final int viewFlags = mViewFlags; + + if ((viewFlags & ENABLED_MASK) == DISABLED) { + // A disabled view that is clickable still consumes the touch + // events, it just doesn't respond to them. + return (((viewFlags & CLICKABLE) == CLICKABLE || + (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); + } + + if (mTouchDelegate != null) { + if (mTouchDelegate.onTouchEvent(event)) { + return true; + } + } + + + if (((viewFlags & CLICKABLE) == CLICKABLE || + (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { + + 处理点击等事件 + return true; + } + + return false; + } + +可以点击或者可以长按事件就会被消费,即使是disable 的。 + + + + + + +# 4 ViewGroup对事件的处理 + +ViewGroup处理分发和处理事件,还有拦截事件的方法 + + +主要是三个方法: + +1. dispatchTouchEvent 分发事件 +2. onInterceptTouchEvent 拦截事件 +3. onTouchEvent 处理事件 + + +而最主要的是dispatchTouchEvent方法: + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + //-------------事件安全的这里不管---------------- + if (!onFilterTouchEventForSecurity(ev)) { + return false; + } + final int action = ev.getAction(); + final float xf = ev.getX(); + final float yf = ev.getY(); + final float scrolledXFloat = xf + mScrollX; + final float scrolledYFloat = yf + mScrollY; + final Rect frame = mTempRect; + //这里是判断是否拦截事件的 mGroupFlags可以通过子类调用方法 requestDisallowInterceptTouchEvent 来改变,表示是否让父View拦截事件 + //稍后再分析requestDisallowInterceptTouchEvent + //这里在down事件时,重置这个标志位,也就是说子view调用requestDisallowInterceptTouchEvent无法影响父view对Down事件的处理。 + boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; + //接下来的一段都和down事件有关,而且对DOWN事件处理至关重要 + if (action == MotionEvent.ACTION_DOWN) {//DOWN事件开始 + //这里把事件接收者 置为null + if (mMotionTarget != null) { + // this is weird, we got a pen down, but we thought it was + // already down! + // XXX: We should probably send an ACTION_UP to the current + // target. + mMotionTarget = null; + } + // If we're disallowing intercept or if we're allowing and we didn't + // intercept + if (disallowIntercept || !onInterceptTouchEvent(ev)) {//这里判断是否拦截事件 只要有一个满足就不拦截事件 + // reset this event's action (just to protect ourselves) + ev.setAction(MotionEvent.ACTION_DOWN); + // We know we want to dispatch the event down, find a child 我们知道想要传递事件,需要找到一个能处理事件的子View , 这里开始查找 + // who can handle it, start with the front-most child. + final int scrolledXInt = (int) scrolledXFloat; + final int scrolledYInt = (int) scrolledYFloat; + final View[] children = mChildren;//所以的子view + final int count = mChildrenCount; + for (int i = count - 1; i>= 0; i--) {//便利查找 + final View child = children[i]; + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE + || child.getAnimation() != null) {//首先 子View是可见的 , 并且没有执行动画的 + child.getHitRect(frame);//获取子View在父View中的范围 + if (frame.contains(scrolledXInt, scrolledYInt)) {//事件是否在子View范围内 + // offset the event to the view's coordinate system + final float xc = scrolledXFloat - child.mLeft; + final float yc = scrolledYFloat - child.mTop; + ev.setLocation(xc, yc);//重新设置事件位置 让事件以子View的顶点为起点 + child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;//改变zi的标志位 + //这里子View对down事件的返回值很重要哦,如果返回false,mMotionTarget就不会得到赋值,接下的MOVE和UP事件都不会传递给子View,返回true就会 + if (child.dispatchTouchEvent(ev)) { + // Event handled, we have a target now. + mMotionTarget = child; + return true; + } + // The event didn't get handled, try the next view. + // Don't reset the event's location, it's not + // necessary here. + } + } + } + } + } + //-----------------------------------Down事件相关的到此结束--------------------------------------------------------- + boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || + (action == MotionEvent.ACTION_CANCEL);//是up事件或者是取消事件 + //重置状态 + if (isUpOrCancel) { + // Note, we've already copied the previous state to our local + // variable, so this takes effect on the next event + mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; + } + // The event wasn't an ACTION_DOWN, dispatch it to our target if + // we have one. + //在刚刚的down事件中没有找到 可以处理事件的目标 ,所以就调用super.dispatchTouchEvent(ev);,即View的dispatchTouchEvent方法 + final View target = mMotionTarget; + if (target == null) { + // We don't have a target, this means we're handling the + // event as a regular view. + ev.setLocation(xf, yf); + if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { + ev.setAction(MotionEvent.ACTION_CANCEL); + mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; + } + return super.dispatchTouchEvent(ev); + } + // if have a target, see if we're allowed to and want to intercept its + // events + if (!disallowIntercept && onInterceptTouchEvent(ev)) {//在MOVE或者UP时,又要拦截事件,或者子view请求拦截事件 + final float xc = scrolledXFloat - (float) target.mLeft; + final float yc = scrolledYFloat - (float) target.mTop; + mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; + ev.setAction(MotionEvent.ACTION_CANCEL);//事件置为cancel + ev.setLocation(xc, yc); + if (!target.dispatchTouchEvent(ev)) {//子view处理取消事件 + // target didn't handle ACTION_CANCEL. not much we can do + // but they should have. + } + // clear the target + mMotionTarget = null;//目标置为null + // Don't dispatch this event to our own view, because we already + // saw it when intercepting; we just want to give the following + // event to the normal onTouchEvent(). + return true; + } + if (isUpOrCancel) {//手指抬起或者取消 把目标置为null + mMotionTarget = null; + } + // finally offset the event to the target's coordinate system and + // dispatch the event. + final float xc = scrolledXFloat - (float) target.mLeft; + final float yc = scrolledYFloat - (float) target.mTop; + ev.setLocation(xc, yc); + //这里判断的是 目标view 是否取消下一个事件 + if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { + ev.setAction(MotionEvent.ACTION_CANCEL); + target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; + mMotionTarget = null; + } + //接下来的事件都交给目标处理 + return target.dispatchTouchEvent(ev); + } + +下面来看一下刚刚说的一个很重要的方法**requestDisallowInterceptTouchEvent,**其实就是改变一个标志位,注意mParent.requestDisallowInterceptTouchEvent(disallowIntercept);他会调用自己的父控件的requestDisallowInterceptTouchEvent方法,然后一直往上调用 + + + + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + + if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { + // We're already in this state, assume our ancestors are too + return; + } + + if (disallowIntercept) { + mGroupFlags |= FLAG_DISALLOW_INTERCEPT; + } else { + mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; + } + + // Pass it up to our parent + if (mParent != null) { + mParent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + +### 总结 +比较重要的是对DOWN事件的处理 +1. 返回true 表示事件已经消费,会继续传递事件 +2. 返回false,表示不处理事件,事件将不会在传递到这里 +3. 只要Down事件返回的是true, 不管之后的MOVE和UP事件返回true或者false 都会得到事件 diff --git "a/Android/UI/View344円275円223円347円263円273円/007342円200円224円342円200円224円View344円272円213円344円273円266円345円210円206円345円217円221円344円270円216円346円272円220円347円240円201円345円210円206円346円236円220円.md" "b/Android/UI/View344円275円223円347円263円273円/007342円200円224円342円200円224円View344円272円213円344円273円266円345円210円206円345円217円221円344円270円216円346円272円220円347円240円201円345円210円206円346円236円220円.md" new file mode 100644 index 0000000..c167c2f --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/007342円200円224円342円200円224円View344円272円213円344円273円266円345円210円206円345円217円221円344円270円216円346円272円220円347円240円201円345円210円206円346円236円220円.md" @@ -0,0 +1,780 @@ +# 1 View的事件分发 + +前面已经大概的分析了2.3版本的源码,现在会对事件分发进行更加深入的学习。 + +再讲一遍废话-_-! + +在Android中使用MotionEvent来表示一个事件,当事件产生后,WMS会通过IPC把事件分发给当前处于活动的窗口,在应用层,最先获取事件的是ViewRoot中的W类,然后通过ViewRoot把事件传递给它内部的mView,然后是Activity。但是对于我们程序员而已,能开始操作事件的起始位置是Activity。所以姑且把Activity作为事件分发的起点。 + + +--- +


+ + + +# 2 事件分发的流程 + +能开始操作事件的起始位置是Activity中的dispatchTouchEvent方法: + + public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + //这是一个空方法 + onUserInteraction(); + } + if (getWindow().superDispatchTouchEvent(ev)) { + return true; + } + return onTouchEvent(ev); + } +可以看到它优先把时间传递给了它内部的Window,其实就是PhoneWindow了,而PhoneWindow的superDispatchTouchEvent方法只是简单的把事件传递给它的DecorView,这里就不贴源码了。大概流程如下: + +`Activity--->Window--->View` + +**如果想屏幕窗口中View的事件,可以从Activity中拦截** + +--- +


+ +# 3 与事件分发的相关的三个方法 + +- dispatchTouchEvent (View,ViewGroup) 传递事件 +- onInterceptouchEvent (ViewGroup) 拦截事件,View不需要拦截事件 +- onTouchEvent (View,ViewGroup) 处理事件 +- 事件传递的返回值,ture表示 拦截不继续 false表示 不拦截,继续流程 +- 事件处理的返回值,true处理了,false由上层View处理 + +--- +


+ + +# 4 View事件分发源码分析 + +在分析之前说明一下事件的分类,MotionEvent描述了用户的行为 + +* ACTION_DOWN **手指按下** +* ACTION_UP **手指抬起** +* ACTION_MOVE **手指移动** +* ACTION_POINTER_DOWN **多指触控时,另一个手指按下** +* ACTION_POINTER_UP **多指触控时,有一个手指抬起** +* ACTION_CANCEL **事件被取消** + + +在一个View树中,最先得到事件的肯定是根View,对于Activity来讲,就是其Window内部的DecorView了,事件通过dispatchTouchEvent方法传递到View中,而根View是ViewGroup类型的,但是ViewGroup的事件分发也依赖于View的事件分发,所以先来分析View的dispatchTouchEvent方法: + +--- +


+ + +### 4.1 View.dispatchTouchEvent方法分析 + + public boolean dispatchTouchEvent(MotionEvent event) { + ...... + //Dwon事件,表示一系列事件的开始,这里要取消嵌套滑动,mNestedScrollingParent置为null + final int actionMasked = event.getActionMasked(); + if (actionMasked == MotionEvent.ACTION_DOWN) { + // Defensive cleanup for new gesture + stopNestedScroll(); + } + //onFilterTouchEventForSecurity一般都成立 + if (onFilterTouchEventForSecurity(event)) { + //noinspection SimplifiableIfStatement + ListenerInfo li = mListenerInfo; + /* + 首先mListenerInfo一般都不会null + 当mOnTouchListener不为null并且其onTouch方法返回true是,将会直接处理事件, + 那么onTouchEvent方法将不会被调用,这也是给我们一个机会在外部优先处理View的事件 + */ + if (li != null && li.mOnTouchListener != null + && (mViewFlags & ENABLED_MASK) == ENABLED + && li.mOnTouchListener.onTouch(this, event)) { + result = true; + } + //当上面不成立,并且onTouchEvent返回true时,表示事件被处理了 + if (!result && onTouchEvent(event)) { + result = true; + } + } + ....... + //如果事件是UP或者Cancel,或者事件是Down并且事件没有被处理停止嵌套滑动 + if (actionMasked == MotionEvent.ACTION_UP || + actionMasked == MotionEvent.ACTION_CANCEL || + (actionMasked == MotionEvent.ACTION_DOWN && !result)) { + stopNestedScroll(); + } + + return result; + } + +分析View的dispatchTouchEvent方法,可知: +View中如果设置了onTouchEventListener,并且我们在onTouch方法中返回true,View的内部将不会再获取事件,这是系统提供给我们的优先处理View事件的接口,如果上述不成立,才会走View内部的onTouchEvent方法,并返回onTouchEvent的返回值 + +接下来分析View.的onTouchEvent方法 + +

+ +####View.onTouchEvent方法分析 + + public boolean onTouchEvent(MotionEvent event) { + final float x = event.getX(); + final float y = event.getY(); + //View的内部标识,一般是View的状态 + final int viewFlags = mViewFlags; + //事件类型 + final int action = event.getAction(); + //这里表示如果View是不可用的,即调用的setDisable(false),或者在xml中设置 + if ((viewFlags & ENABLED_MASK) == DISABLED) { + if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {/恢复按下的状态 + setPressed(false); + } + //可以看出,只要View是可以点击或者可以长按的,事件还是会被处理,但是不会响应 + //View的click或者longClick事件 + return (((viewFlags & CLICKABLE) == CLICKABLE + || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) + || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); + } + //如果有mTouchDelegate,优先让其处理 + if (mTouchDelegate != null) { + if (mTouchDelegate.onTouchEvent(event)) { + return true; + } + } + //最后如果View是可以点击或者长按的,则会进入,且此代码块必返回true,表示事件使用被处理 + if (((viewFlags & CLICKABLE) == CLICKABLE || + (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || + (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { + switch (action) { + //抬起事件 + case MotionEvent.ACTION_UP: + //按下了或者预按下了(不懂) + boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; + //处于按下的状态 + if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { + //获取焦点如果可以获取焦点并且触摸模式可以获取焦点,并且当前没有获取到焦点 + boolean focusTaken = false; + if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { + //请求焦点,如果已有返回false + focusTaken = requestFocus(); + } + //跟涟漪有关 + if (prepressed) { + // The button is being released before we actually + // showed it as pressed. Make it show the pressed + // state now (before scheduling the click) to ensure + // the user sees it. + setPressed(true, x, y); + } + if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { + // This is a tap, so remove the longpress check + removeLongPressCallback(); + + // 这里很重要,如果View处于可获取焦点状态,但是他没有获取到焦点,那么第一次点击它时,不会调用它的click事件 + if (!focusTaken) { + //下面是处理点击事件 + // Use a Runnable and post this rather than calling + // performClick directly. This lets other visual state + // of the view update before click actions start. + if (mPerformClick == null) { + mPerformClick = new PerformClick(); + } + if (!post(mPerformClick)) { + performClick(); + } + } + } + + if (mUnsetPressedState == null) { + mUnsetPressedState = new UnsetPressedState(); + } + //接下来的恢复按下状态 + if (prepressed) { + postDelayed(mUnsetPressedState, + ViewConfiguration.getPressedStateDuration()); + } else if (!post(mUnsetPressedState)) { + // If the post failed, unpress right now + mUnsetPressedState.run(); + } + + removeTapCallback(); + } + mIgnoreNextUpEvent = false; + break; + + case MotionEvent.ACTION_DOWN: + mHasPerformedLongPress = false; + + if (performButtonActionOnTouchDown(event)) { + break; + } + + //一般返回ture + boolean isInScrollingContainer = isInScrollingContainer(); + + // For views inside a scrolling container, delay the pressed feedback for + // a short period in case this is a scroll. + if (isInScrollingContainer) { + mPrivateFlags |= PFLAG_PREPRESSED; + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); + } + mPendingCheckForTap.x = event.getX(); + mPendingCheckForTap.y = event.getY(); + postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + } else { + // Not inside a scrolling container, so show the feedback right away + setPressed(true, x, y); + checkForLongClick(0); + } + break; + + case MotionEvent.ACTION_CANCEL: + //事件被取消,移除所有回调,恢复状态 + setPressed(false); + removeTapCallback(); + removeLongPressCallback(); + mInContextButtonPress = false; + mHasPerformedLongPress = false; + mIgnoreNextUpEvent = false; + break; + + case MotionEvent.ACTION_MOVE: + drawableHotspotChanged(x, y); + //事件移除边界,直接恢复状态,移除callBack,不会触发点击和长按事件 + // Be lenient about moving outside of buttons + if (!pointInView(x, y, mTouchSlop)) { + // Outside button + removeTapCallback(); + if ((mPrivateFlags & PFLAG_PRESSED) != 0) { + // Remove any future long press/tap checks + removeLongPressCallback(); + + setPressed(false); + } + } + break; + } + //只要可以点击或者长按,就消费事件 + return true; + } + //否则不消费事件 + return false; + } + + +分析View.的onTouchEvent事件可知: +- View是否可用不影响事件的处理,只要View是可以点击的或者长按的,则View就会消费事件,但是如果View不可用,则不会触发点击或长按事件 +- focus对View的点击事件有影响,View是isFocusable的并且isFocusableInTouchMode的并且当前没有获取到焦点,则先回请求焦点,此次点击不会响应click等事件 + + +由于View没有子View,所以不需要拦截事件,没有拦截事件的方法。 + +分析完View的事件分发在来分析ViewGroup的事件分发 + +--- +


+ + + +--- +### 3.2 ViewGroup的事件分发 + +ViewGroup继承自View,比View多了一个onInterceptTouchEvent,只重写了View的dispatchTouchEvent方法,所以默认ViewGroup的事件处理和View是一样的,只是改变了事件的分发逻辑,因为它有子View + +####ViewGroup的onInterceptTouchEvent + + public boolean onInterceptTouchEvent(MotionEvent ev) { + return false; + } + +可以看出默认的ViewGroup不会拦截事件 + + +###ViewGroup的dispatchTouchEvent方法 + +在了解dispatchTouchEvent方法前,先来了解一下TouchTarget,类似Handler中的Message的回收复用机制,用来记录处理事件的子view: + + + private static final class TouchTarget { + private static final int MAX_RECYCLED = 32; + private static final Object sRecycleLock = new Object[0]; + private static TouchTarget sRecycleBin; // 回收再利用的链表头 + private static int sRecycledCount; + + public static final int ALL_POINTER_IDS = -1; // all ones + + // 处理事件子view + public View child; + public int pointerIdBits; + public TouchTarget next;//指向链表中的下一个 + + private TouchTarget() { + } + + // Message里也有类似的实现 + public static TouchTarget obtain(View child, int pointerIdBits) { + final TouchTarget target; + synchronized (sRecycleLock) { + if (sRecycleBin == null) { // 没有可以回收的目标,则new一个返回 + target = new TouchTarget(); + } else { + target = sRecycleBin; // 重用当前的sRecycleBin + sRecycleBin = target.next; // 更新sRecycleBin指向下一个 + sRecycledCount--; // 重用了一个,可回收的减1 + target.next = null; // 切断next指向 + } + } + target.child = child; // 找到合适的target后,赋值 + target.pointerIdBits = pointerIdBits; + return target; + } + + public void recycle() { // 回收过程 + synchronized (sRecycleLock) { + if (sRecycledCount < MAX_RECYCLED) {//没有超过链表长度 + next = sRecycleBin; // next指向旧的可回收的头 + sRecycleBin = this; // update旧的头指向this,表示它自己现在是可回收的target(第一个) + sRecycledCount += 1; // 多了一个可回收的 + } else { + next = null; // 没有next了 + } + child = null; // 清空child字段 + } + } + } + +**dispatchTouchEvent方法**: + + public boolean dispatchTouchEvent(MotionEvent ev) { + ...... + //记录事件是否被处理 + boolean handled = false; + if (onFilterTouchEventForSecurity(ev)) {//这里一般都成立 + final int action = ev.getAction(); + final int actionMasked = action & MotionEvent.ACTION_MASK; + + // 预处理刚开始的down事件 + if (actionMasked == MotionEvent.ACTION_DOWN) { + //一个新的系类事件开始,清除所有之前的状态和事的处理者 + cancelAndClearTouchTargets(ev); + //清理所有状态,包括requestDisallowInterceptTouchEvent影响的标志位 + resetTouchState(); + } + //----第一段完 + + + //检查是否拦截事件,这里分两种情况: + //1是对down事件的处理,判断是否要拦截事件,这是touchTarget是为null的,且requestDisallowInterceptTouchEvent是没有作用的 + //2是对其他事件的处理,这时已经不是down事件,但是可能已经找到处理事件的子view,在把接下来的事件传递给子view时,始终要判断自己是否需要拦截,这里requestDisallowInterceptTouchEvent是有用的 + final boolean intercepted; + //如果是down事件,或者mFirstTouchTarget不为null。Down事件时mFirstTouchTarget肯定是为null的。 + if (actionMasked == MotionEvent.ACTION_DOWN + || mFirstTouchTarget != null) { + /*首先判断标志位disallowIntercept是否允许拦截事件,这个标志位可以通过requestDisallowInterceptTouchEvent改变,默认是false + */ + final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; + //如果可以拦截事件 + if (!disallowIntercept) { + //调用onInterceptTouchEvent方法 + intercepted = onInterceptTouchEvent(ev); + ev.setAction(action); // restore action in case it was changed + } else { + //如果直接不允许拦截事件,就不拦截事件,可见标志位优先级大于拦截方法 + intercepted = false; + } + } else { + //如果不是down事件,而且没有找到可以处理事件的子view,以后直接自己处理事件 + intercepted = true; + } + + ...... + + // 检查事件的取消 + final boolean canceled = resetCancelNextUpFlag(this) + || actionMasked == MotionEvent.ACTION_CANCEL; + + // 安卓3.0引入的拆分事件,-_-! + final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; + //记录事件处理者 + TouchTarget newTouchTarget = null; + //是否开始分发事件到新的target + boolean alreadyDispatchedToNewTouchTarget = false; + //事件没有被取消,并且不拦截事件,那么找能处理事件的孩子 + if (!canceled && !intercepted) { + + View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() + ? findChildWithAccessibilityFocus() : null; + + if (actionMasked == MotionEvent.ACTION_DOWN//down事件 + || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)//接下来的down事件 + || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { + + //下面的逻辑到Dispatch to touch targets都在down事件内 + + final int actionIndex = ev.getActionIndex(); //down事件始终是0 + final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) + : TouchTarget.ALL_POINTER_IDS; + + // Clean up earlier touch targets for this pointer id in case they + // have become out of sync. + removePointersFromTouchTargets(idBitsToAssign); + + final int childrenCount = mChildrenCount; + //一般都成立 + if (newTouchTarget == null && childrenCount != 0) { + final float x = ev.getX(actionIndex); + final float y = ev.getY(actionIndex); + // Find a child that can receive the event. + // Scan children from front to back. + final ArrayList preorderedList = buildOrderedChildList(); + final boolean customOrder = preorderedList == null + && isChildrenDrawingOrderEnabled(); + final View[] children = mChildren; + //从外到内找可以处理事件的子view + for (int i = childrenCount - 1; i>= 0; i--) { + final int childIndex = customOrder + ? getChildDrawingOrder(childrenCount, i) : i; + final View child = (preorderedList == null) + ? children[childIndex] : preorderedList.get(childIndex); + + // safer given the timeframe. + if (childWithAccessibilityFocus != null) { + if (childWithAccessibilityFocus != child) { + continue; + } + childWithAccessibilityFocus = null; + i = childrenCount - 1; + } + //如果不可以接收事件,跳出此次循环 + if (!canViewReceivePointerEvents(child) + || !isTransformedTouchPointInView(x, y, child, null)) { + ev.setTargetAccessibilityFocus(false); + continue; + } + //找到了一个可以接收事件的子view,这里是个检查newTouchTarget一般返回null + newTouchTarget = getTouchTarget(child); + if (newTouchTarget != null) { + // Child is already receiving touch within its bounds. + // Give it the new pointer in addition to the ones it is handling. + newTouchTarget.pointerIdBits |= idBitsToAssign; + break; + } + + resetCancelNextUpFlag(child); + //这里把事件交给可以接收事件的子view处理,如果子view在down事件中返回true,mFirstTouchTarget将会被赋值 + if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { + // Child wants to receive touch within its bounds. + mLastTouchDownTime = ev.getDownTime(); + if (preorderedList != null) { + + for (int j = 0; j < childrenCount; j++) { + if (children[childIndex] == mChildren[j]) { + mLastTouchDownIndex = j; + break; + } + } + } else { + mLastTouchDownIndex = childIndex; + } + mLastTouchDownX = ev.getX(); + mLastTouchDownY = ev.getY(); + //对mFirstTouchTarget将会被赋值 + newTouchTarget = addTouchTarget(child, idBitsToAssign); //标志已经分发事件到处理者。 + alreadyDispatchedToNewTouchTarget = true; + break; + } + + // The accessibility focus didn't handle the event, so clear + // the flag and do a normal dispatch to all children. + ev.setTargetAccessibilityFocus(false); + } + if (preorderedList != null) preorderedList.clear(); + } + // 将此事件交给child处理,有这种情况,一个手指按在了child1上,另一个手指按在了child2上,以此类推,这样TouchTarget的链就形成了 + if (newTouchTarget == null && mFirstTouchTarget != null) { + // Did not find a child to receive the event. + // Assign the pointer to the least recently added target. + newTouchTarget = mFirstTouchTarget; + while (newTouchTarget.next != null) { + newTouchTarget = newTouchTarget.next; + } + newTouchTarget.pointerIdBits |= idBitsToAssign; + } + } + } + + //第二段完 ,应该判断是否已经找到了事件处理者 + + + + + // Dispatch to touch targets.开始分发事件,这里是非down事件,不会走上面逻辑 + //这里有有子view可以处理事件 + if (mFirstTouchTarget == null) { + //这里会自己处理事件 + // No touch targets so treat this as an ordinary view. + handled = dispatchTransformedTouchEvent(ev, canceled, null, + TouchTarget.ALL_POINTER_IDS); + } else { + + //否则把事件交给子view处理 + TouchTarget predecessor = null; + TouchTarget target = mFirstTouchTarget; + while (target != null) { + final TouchTarget next = target.next; + if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { + handled = true; + } else { + //这里还是会判断是否拦截事件 + final boolean cancelChild = resetCancelNextUpFlag(target.child) + || intercepted; + //继续分发,但是传入的参数cancelChild很重要 + if (dispatchTransformedTouchEvent(ev, cancelChild, + target.child, target.pointerIdBits)) { + handled = true; + } + //如果取消child的事件,事件交给处理链的下一个处理者, + //一般只有一个,这时候mFirstTouchTarget将=null + if (cancelChild) { + if (predecessor == null) { + mFirstTouchTarget = next; + } else { + predecessor.next = next; + } + target.recycle(); + target = next; + continue; + } + } + predecessor = target; + target = next; + } + } + + //第三段完 + + //扫尾工作 + // Update list of touch targets for pointer up or cancel, if needed. + if (canceled + || actionMasked == MotionEvent.ACTION_UP + || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { + //手指抬起,重置状态,包括拦截标识 + resetTouchState(); + } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { + //另一个手指抬起,清除对应处理链 + final int actionIndex = ev.getActionIndex(); + final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); + removePointersFromTouchTargets(idBitsToRemove); + } + } + + if (!handled && mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); + } + //返回结果 + return handled; + } + + +再来看一下**dispatchTransformedTouchEvent**,分发事件的主要逻辑 + + private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, + View child, int desiredPointerIdBits) { + final boolean handled; + final int oldAction = event.getAction(); + //如果事件是cancel事件 + if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { + event.setAction(MotionEvent.ACTION_CANCEL); + if (child == null) { + //子view是null,自己处理 + handled = super.dispatchTouchEvent(event); + } else { + //给子view一个cancel事件 + handled = child.dispatchTouchEvent(event); + } + event.setAction(oldAction); + return handled; + } + + if (newPointerIdBits == 0) { + return false; + } + + final MotionEvent transformedEvent;//多点事件的处理 + if (newPointerIdBits == oldPointerIdBits) { + if (child == null || child.hasIdentityMatrix()) { + if (child == null) { + handled = super.dispatchTouchEvent(event); + } else { + final float offsetX = mScrollX - child.mLeft; + final float offsetY = mScrollY - child.mTop; + event.offsetLocation(offsetX, offsetY); + handled = child.dispatchTouchEvent(event); + event.offsetLocation(-offsetX, -offsetY); + } + return handled; + } + transformedEvent = MotionEvent.obtain(event); + } else { + transformedEvent = event.split(newPointerIdBits); + } + + // 一般从这里分发事件 + if (child == null) {//如果子view是null,自己处理 + handled = super.dispatchTouchEvent(transformedEvent); + } else {//矫正位置后,交给ziview处理 + final float offsetX = mScrollX - child.mLeft; + final float offsetY = mScrollY - child.mTop; + transformedEvent.offsetLocation(offsetX, offsetY); + if (! child.hasIdentityMatrix()) { + transformedEvent.transform(child.getInverseMatrix()); + } + handled = child.dispatchTouchEvent(transformedEvent); + } + + // Done. + transformedEvent.recycle(); + //然会结果 + return handled; + } + + +然后关于**TouchTarget**的一些操作了: + + //清理拦截事件的标志位等 + private void resetTouchState() { + clearTouchTargets(); + resetCancelNextUpFlag(this); + mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; + } + + /** + * 清理取消接下来的事件的标志位 + */ + private static boolean resetCancelNextUpFlag(View view) { + if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) { + view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT; + return true; + } + return false; + } + + /** + *清理所有的事件处理者 + */ + private void clearTouchTargets() { + TouchTarget target = mFirstTouchTarget; + if (target != null) { + do { + TouchTarget next = target.next; + target.recycle(); + target = next; + } while (target != null); + mFirstTouchTarget = null; + } + } + + /** + * 取消之前所有事件接收者的事件 + */ + private void cancelAndClearTouchTargets(MotionEvent event) { + if (mFirstTouchTarget != null) { + boolean syntheticEvent = false; + if (event == null) { + final long now = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + event.setSource(InputDevice.SOURCE_TOUCHSCREEN); + syntheticEvent = true; + } + + for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) { + resetCancelNextUpFlag(target.child); + dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits); + } + clearTouchTargets(); + if (syntheticEvent) { + event.recycle(); + } + } + } + + /** + * 从事件处理链中获取一个处理者 + */ + private TouchTarget getTouchTarget(View child) { + for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) { + if (target.child == child) { + return target; + } + } + return null; + } + + /** + * 把一个事件处理者添加到事件处理链中。 + */ + private TouchTarget addTouchTarget(View child, int pointerIdBits) { + TouchTarget target = TouchTarget.obtain(child, pointerIdBits); + target.next = mFirstTouchTarget; + mFirstTouchTarget = target; + return target; + } + + // 从链表中删除某个特定的节点 + private void cancelTouchTarget(View view) { + TouchTarget predecessor = null; + TouchTarget target = mFirstTouchTarget; + while (target != null) { + final TouchTarget next = target.next; + if (target.child == view) { + if (predecessor == null) { + mFirstTouchTarget = next; + } else { + predecessor.next = next; + } + target.recycle(); + + final long now = SystemClock.uptimeMillis(); + MotionEvent event = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + event.setSource(InputDevice.SOURCE_TOUCHSCREEN); + view.dispatchTouchEvent(event); + event.recycle(); + return; + } + predecessor = target; + target = next; + } + } + + + +--- +


+ + +然后的看一下**requestDisallowInterceptTouchEvent**方法 + + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + + if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { + // We're already in this state, assume our ancestors are too + return; + } + + if (disallowIntercept) { + mGroupFlags |= FLAG_DISALLOW_INTERCEPT; + } else { + mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; + } + + // Pass it up to our parent + if (mParent != null) { + mParent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + + +至此大概的流程我们分析完了,可以看到,虽然代码与2.3版本的相比是改变了不少,但是处理事件的主要流程并没有改变(怎么可能改变),经过分析,我们可以得出很多的结论,下篇继续-_-. + +--- diff --git "a/Android/UI/View344円275円223円347円263円273円/008342円200円224円342円200円224円345円205円263円344円272円216円View344円272円213円344円273円266円345円210円206円345円217円221円347円232円204円346円200円273円347円273円223円344円270円216円346円273円221円345円212円250円345円206円262円347円252円201円.md" "b/Android/UI/View344円275円223円347円263円273円/008342円200円224円342円200円224円345円205円263円344円272円216円View344円272円213円344円273円266円345円210円206円345円217円221円347円232円204円346円200円273円347円273円223円344円270円216円346円273円221円345円212円250円345円206円262円347円252円201円.md" new file mode 100644 index 0000000..e01676e --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/008342円200円224円342円200円224円345円205円263円344円272円216円View344円272円213円344円273円266円345円210円206円345円217円221円347円232円204円346円200円273円347円273円223円344円270円216円346円273円221円345円212円250円345円206円262円347円252円201円.md" @@ -0,0 +1,106 @@ +# 1 事件分发流程 + +通过前面的事件分发研究,我们可以总结出事件分发的流程: + +在ViewGroup的dispatchTouchEvent中 + +1. 处理DOWN事件,在down事件中,如果拦截了事件则自己处理(onTouchEvent方法被调用),子无法在获取事件了,如果不拦截DOWN事件,则会从外到内查找是否有子view能处理事件,如果有一个子view可以处理事件(down返回true),则接下来的事件交给子view处理,ViewGroup的onInterceptTouchEvent方法还是会被调用,一旦其返回true,那么ViewGroup开始拦截事件,而子view以一个cancel事件结束 + +2. 接下来的move和up事件,如果在down中没有找到可以处理事件的子view,则自己处理接下来的事件。 + +3. 如果有子view可以处理事件,并且不拦截事件,则把事件都交给子view处理,一旦ViewGroup开始拦截,那么接收事件的子view将会被赋值为null,接下来事件遵循第二点。 + +4. 如果子view能接收到DWON事件,并且在接收到事件事件后,调用requestDisallowInterceptTouchEvent(true)方法,ViewGroup无法再拦截事件,也就说requestDisallowInterceptTouchEvent优先级高于onInterceptTouchEvent,但是requestDisallowInterceptTouchEvent不能干预父view对DOWN事件的处理。对于DOWN事件onInterceptTouchEvent说了算。 + + + + + +# 2 关于事件分发的规律总结(参考Android开发艺术探索) + + +1. **同一个事件序列**是指从手指触摸屏幕那一刻起,到手指离开屏幕的那一刻结束,所以一些列事件由: +`一个DOWN + 不数量的MOVE + 一个UP事件(可能CANCEL) ` +组成 + +2. 正常情况下,**一个事件只能被一个View拦截和消费**,也就是说同一个事件不可能被两个View共同来消费,但是如果一个View接收到事件并处理后有分发给其他View处理除外。 + +3. 如果一个ViewGroup能接收到事件,并且开始拦截事件,那么这一系列事件只能由它来处理。并且他的onInterceptTouchEvent方法不会再被调用。 + + 关于ViewGroup能否接收到事件又分为两种: + - 在DOWN就开始拦截事件 + - 在DOWN没有拦截事件,但是子view处理了DOWN事件并且没有改变FLAG_DISALLOW_INTERCEPT这个标志位来不允许父View拦截事件,之后ViewGroup的onInterceptTouchEvent依然会被调用,如果返回true,ViewGroup还是可以拦截事件,之后可接收事件的子View收到一个CANCEL事件,然后在ViewGroup中被置为null + +4. FLAG_DISALLOW_INTERCEPT 和 touchTarget在一系列事件的开始和结尾都会被重置,也就是说子View无法使用requestDisallowInterceptTouchEvent方法来要影响DOWN事件,如果ViewGroup在DOWN就开始拦截事件,子view不可能再得到事件 + +5. 如果一个View开始接收事件,如果它不消费DOWN事件(DOWN中返回false),那么它不会接收到同系列事件中接下来的事件,在源码中的体现就是,ViewGroup在DOWN事件中没有找到可以处理事件的子view,接下来的同系列事件就会自己处理,即他的onTouchEvent方法被调用 + +6. 如果一个View开始接收事件,如果它消费了DOWN事件(DOWN中返回true),但是接下来的事件它返回false,这个View依然能继续接收这一系列的事件,直到UP(或CANCEL)事件结束,最终事件会回到Activity的onTouchEvent方法,由Activity处理 + +7. View不拦截事件,它接收到事件会里面调用onTouchEvent方法,ViewGroup默认不拦截事件 + +8. 如果一个View是可以被click或者longClick的,那么它的onTouchEvent方法默认都会消费事件,即使它是不可用的(disable),disable只会导致click或者longClick不被调用: + - onClick发生前提,View可以点击,View能接收到Down和Up事件 + +9. focus对View的点击事件有影响,View的isFocusable和isFocusableInTouchMode为true并且当前没有获取到焦点,则会先请求焦点,此次点击不会响应click等事件 + +10. 事件是由外到内进行传递的,由内到外进行处理的,即事件总是先传给根View,再由根View传递给子View,而默认的处理顺序是子View到根View,ViewGroup可以全部拦截事件,子View可以调用requestDisallowInterceptTouchEvent干预父View的事件分发(DOWN事件无法被干预) + +11. 当然对于某些特殊的需求,系统的dispatchTouchEvent方法可能不适用,那么需要重写ViewGroup的dispatchTouchEvent方法,那么事件分发的逻辑完全有我们定义。只要ViewGroup能接收到事件,它的dispatchTouchEvent每次都会被调用。 + +12. 如果ACTION_DOWN事件发生在某个View的范围之内,则后续的ACTION_MOVE,ACTION_UP和ACTION_CANCEL等事件都将被发往该View,即使事件已经出界了。 + +13. 第一根按下的手指触发ACTION_DOWN事件,之后按下的手指触发ACTION_POINTER_DOWN事件,中间起来的手指触发ACTION_POINTER_UP事件,最后起来的手指触发ACTION_UP事件(即使它不是触发ACTION_DOWN事件的那根手指)。 + +14. pointer id可以用于跟踪手指,从按下的那个时刻起pointer id生效,直至起来的那一刻失效,这之间维持不变(后续MotionEvent会详细解读)。 + +15. 如果父View在onInterceptTouchEvent中拦截了事件,则onInterceptTouchEvent中不会再收到Touch事件了,事件被直接交给它自己处理(按照普通View的处理方式)。 + +16. 如果一个事件首先由子view处理,但是如果子view在处理的过程中某个时刻返回了false,则此事件序列全部交给Activity处理。 + + +# 3 关于事件分发中的滑动冲突 + +常见的滑动冲突场景: + +- 1,外部滑动方向与内部滑动方向不一致 +- 2,内部滑动方向与外部滑动方向一致 +- 3,上述两种情况的嵌套 + + +####滑动冲突场景: + +场景1: + +![](img/008_左右上下冲突.png) + +类似ViewPager与多个ListFragemnt嵌套 + + +场景2: + + +![](img/008_同向冲突.png) + +类似ViewPager与ViewPager的嵌套 + +场景3: + +![](img/008_复杂冲突.png) + +类似SlidMenu加ViewPager加ListFragment + + + + + + +###解决滑动冲突的规则 + +- 对于场景1有以下方法来解决 + - 判断滑动路径与水平方向夹角 + - 判断水平方向与垂直方向的距离差(常用) + - 判断水平方向与垂直方向的速度差 + +- 对于场景2 + - 这能通过业务需求来解决,比如某个情况只允许哪个View滑动 diff --git "a/Android/UI/View344円275円223円347円263円273円/009342円200円224円342円200円224円View345円256円236円347円216円260円346円273円221円345円212円250円347円232円204円346円226円271円345円274円217円.md" "b/Android/UI/View344円275円223円347円263円273円/009342円200円224円342円200円224円View345円256円236円347円216円260円346円273円221円345円212円250円347円232円204円346円226円271円345円274円217円.md" new file mode 100644 index 0000000..6f2ce22 --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/009342円200円224円342円200円224円View345円256円236円347円216円260円346円273円221円345円212円250円347円232円204円346円226円271円345円274円217円.md" @@ -0,0 +1,232 @@ +# View实现滑动的方式以及对View内部变量的影响 + +- ScrollTo/ScrollBy +- offsetTopAndBottom和offsetLeftAndRight +- layoutParams +- onLayout +- Scroller +- ViewDragHelper +- 动画 + + +--- + +##1,ScrollTo/ScrollBy + +这两个方法移动的是View的content,如果在ViewGroup中移动的是ViewGroup的所有子view。如果在View中使用,那么移动的就是View的内容,类如TextView,content就是它的文本,ImageView,content就是他的drawable + +scrollTo/scrollBy方法虽然导致view的内容区域移动,但是一般不会导致子视图的重绘,此时绘制的是View的缓存。 + +###View的视图移动理解 + +> 手机屏幕是一个中空的盖板,盖板下面是一个巨大的画布,也就是我们需要显示的内容,把这个盖板盖在画布的某一处时,透过中间的矩形,我们看见了手机屏幕上显示的视图,而画布上的其他视图,被盖住了无法看见,在手机屏幕上,我们看不见的视图,不代表它不存在,可能就是被盖住了(在屏幕外面),当调用scrollTo方法时,可以理解为外面的盖板在移动,比如手指在手机屏幕上往下滑动的时候,使用scrollTo/scrollBy方法来处理滑动的话,你滑动的是那个遮罩,如果希望content往下移动,那么遮罩就应该是往上面移动,刚好与手指的滑动方向相反,体现在计算中就是对每次滑动的偏移值取反。 + +###实现方法: + + //记录手指按下的位置 + private int lastX, lastY; + @Override + public boolean onTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + lastX = x; + lastY = y; + break; + case MotionEvent.ACTION_MOVE: + //计算出手指的距离 + int dx = x - lastX; + int dy = y - lastY; + //调用scrollBy实现滑动 + scrollBy(-dx , -dy); + break; + case MotionEvent.ACTION_UP: + break; + } + lastY = y; + lastX = x; + return true; + } + +####对View内部变量的影响: + +调用scrollTo/scrollBy滑动的是View的content,影响的是View的**mScrollX**与**mScrollY**。这时将触发**onScrollChanged**,在ViewGroup中调用scrollTo/scrollBy,不会对子view的各种参数如:top,left,x,y等造成影响。mScrollX、mScrollY就是**视图的内容的偏移量,而不是视图相对于其他容器或者视图的偏移量**。mScrollX的值总是等于View的左边缘与View的内容左边缘的水平方向距离。mScrollY类似。 + + + + +--- +


+ +## 2,offsetTopAndBottom和offsetLeftAndRight + +offsetTopAndBottom和offsetLeftAndRight是实实在在的改变View在ViewGroup中的位置。 + +####实现方式: + + private int lastX, lastY; + @Override + public boolean onTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + lastX = x; + lastY = y; + break; + case MotionEvent.ACTION_MOVE: + int dx = x - lastX; + int dy = y - lastY; + getChildAt(0).offsetTopAndBottom(dy); + getChildAt(0).offsetLeftAndRight(dx); + break; + case MotionEvent.ACTION_UP: + break; + } + lastY = y; + lastX = x; + return true; + } + +####对View内部变量的影响: +view自身调用了offsetTopAndBottom和offsetLeftAndRight方法对应其内部的**top,bottom,left,right,x,y**都会素质改变 + + + + + +--- +


+ +## 3,LayoutParams + +每一个View都有其LayoutParams对应的LayoutParams,具体的LayoutParams跟它的父布局有关,LayoutParams描述了View一系列信息,保存View的大小,位置等。所以可以通过动态的改变这些数据来达到View的滑动,当然LayoutParams不止可以做这些。 + +####实现方式: + + private int lastX, lastY; + + @Override + public boolean onTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + lastX = x; + lastY = y; + break; + case MotionEvent.ACTION_MOVE: + int dx = x - lastX; + int dy = y - lastY; + MarginLayoutParams marginLayoutParams = (MarginLayoutParams) getChildAt(0).getLayoutParams(); + + marginLayoutParams.leftMargin += dx; + marginLayoutParams.topMargin += dy; + + getChildAt(0).setLayoutParams(marginLayoutParams); + + break; + case MotionEvent.ACTION_UP: + break; + } + lastY = y; + lastX = x; + return true; + } + + +####对View内部变量的影响: +改变了View的布局参数,肯定其内部的参数也是改变了,同第二种方法 + + +--- +


+## 4,onLayout + +在ViewGroup中,通过onLayout来确定来确定子view的位置,所以我们也可以通过反复的调用这个方法来实现子View的滑动 + + +####实现方式: + + private int lastX, lastY; + + @Override + public boolean onTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + lastX = x; + lastY = y; + break; + case MotionEvent.ACTION_MOVE: + int dx = x - lastX; + int dy = y - lastY; + + int l = getChildAt(0).getLeft() + dx; + int t = getChildAt(0).getTop() + dy; + getChildAt(0).layout(l, t, l + getChildAt(0).getWidth(), t + getChildAt(0).getHeight()); + + break; + case MotionEvent.ACTION_UP: + break; + } + lastY = y; + lastX = x; + return true; + } + +####对View内部变量的影响: +改变了View的布局位置,肯定其内部的参数也是改变了,同第二种方法 + + + + + +--- +


+## 5,Scroller与OverScroller +OverScroller是对Scroller的功能增强,其是它们只是一个辅助计算的工具,告诉我们在某一时刻View应该在哪一个位置,我们还是要调用**scrollTo**方法来实现滑动. +调用过程: +`invalidate()-->onDraw()-->computeScroll()` + +![](img/009_scroller.jpg) +具体实现的方式就不贴了, + + +--- +


+## 6,ViewDragHelper +ViewDragHelper位于support v4包中,DrawerLayoou和SlidingPaneLayout就是用ViewDrawHelper实现的,ViewDragHelper的功能非常强大,会在另外的笔记中详细学习,其内部也是使用的offsetTopAndBottom和offsetLeftAndRight方法。 + + + + +--- +


+## 7,使用动画 + +使用动画来实现View的滑动,主要是改变View的translationX和translationY,既可以使用传统的动画也可以使用3.0的属性动画,如果需要兼容到3.0一下,考虑使用nineOldAndroid开源项目 + +- 传统动画只是改变View的影响,View的位置参数并没有改变,包括View的位置参数都没有改变,而使用3.0的属性动画则不会。 + +- nineOldAndroid本质上还是View的动画 + +一句简单的代码即可实现View的滑动: + + view.animate().translationX(300).translationY(-300).setDuration(4000).start(); + + +####对View内部变量的影响: +对top,bottom,right,left没有影响,会改变x,y,translationX和translationY. + +也就是这个公式了: +x = left + translationX +y = top + translationY + +动画适合做没有交互的View的滑动效果。 diff --git "a/Android/UI/View344円275円223円347円263円273円/010342円200円224円342円200円224円View346円273円221円345円212円250円345円206円262円347円252円201円345円270円270円347円224円250円350円247円243円345円206円263円346円226円271円346円241円210円.md" "b/Android/UI/View344円275円223円347円263円273円/010342円200円224円342円200円224円View346円273円221円345円212円250円345円206円262円347円252円201円345円270円270円347円224円250円350円247円243円345円206円263円346円226円271円346円241円210円.md" new file mode 100644 index 0000000..da6e54c --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/010342円200円224円342円200円224円View346円273円221円345円212円250円345円206円262円347円252円201円345円270円270円347円224円250円350円247円243円345円206円263円346円226円271円346円241円210円.md" @@ -0,0 +1,549 @@ +# 1 外部拦截法 + +外部拦截法即事件都经过父容器处理,如果父容器需要事件就处理事件,不需要则不拦截,下面来看一下伪代码: + + + @Override + public boolean onInterceptHoverEvent(MotionEvent event) { + boolean intercept = false; + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + //如果希望子view能接收到事件,DOWN必然要返回false + intercept = false; + break; + + case MotionEvent.ACTION_MOVE: + //如果需要拦截事件,就返回true + if (needIntercept(event)) { + intercept = true; + } else { + intercept = false; + } + break; + + case MotionEvent.ACTION_UP: + //手指抬起,必须返回false,因为返回值对自己没有影响,而对子view可能有影响 + intercept = false; + break; + } + //重新设置最后一次位置 + mLastEventX = x; + mLastEventY = y; + return intercept; + } + + private boolean needIntercept(MotionEvent event) { + return false; + } + +下面来分析一下这段伪代码的意思: + +1. 首先ACTION_DOWN必须返回false,否则子view无法接收到事件,事件都会由自己处理 +2. 对应ACTION_MOVE则对自己根据情况处理,需要就拦截,否则不拦截 +3. 最后是ACTION_UP,必须返回false,原因有: + - ACTION_UP的返回值对自身并没有影响,自身始终能接收到事件 + - 如果子一些列事件中,ViewGroup都始终没有拦截事件,却在ACTION_UP中返回true,这样导致子view无法接收到UP事件,那么就会影响子view的click事件,或者其他逻辑处理 +4. 是否需要拦截事件都交给needIntercept方法处理,这个处理是根据业务来处理的,还可如果我们无法确定某些因素,还可以通过设置回调接口来处理,让其他对象通过接口来告知感兴趣的事。 + + 如下面代码: + + private boolean needIntercept(MotionEvent event) { + if (mEventCallback != null) { + return mEventCallback.isCanIntercept(); + } + return false; + } + + public EventCallback mEventCallback; + + public void setEventCallback(EventCallback eventCallback) { + mEventCallback = eventCallback; + } + public interface EventCallback{ + boolean isCanIntercept(); + } + +在外部拦截法中,子view最好不要使用requestDisallowInterceptTouchEvent来干预事件的处理 + + + + + + +# 2 内部拦截法 + +内部拦截是指父容器不拦截任何事件,所有事件都传递给子view,如果子元素需要事件就直接消耗,否则交给父容器处理,这种拦截法需要配合requestDisallowInterceptTouchEvent方法来使用。我们需要重写子view的dispatchTouchEvent方法。 + + private int mLastX, mLastY; + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + int action = event.getActionMasked(); + int x = (int) event.getX(); + int y = (int) event.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + //不让父View拦截事件 + mLastX = x; + mLastY = y; + getParent().requestDisallowInterceptTouchEvent(true); + break; + + case MotionEvent.ACTION_MOVE: + //如果需要拦截事件,就返回true + if (!needIntercept(event)) { + getParent().requestDisallowInterceptTouchEvent(false); + } + break; + + case MotionEvent.ACTION_UP: + //手指抬起,必须返回false,因为返回值对自己没有影响,而对子view可能有影响 + break; + } + mLastX = x; + mLastY = y; + return super.dispatchTouchEvent(event); + } + + +代码说明: +- 首先,必须假定父view不拦截DOWN事件而拦截其他事件,否则子view无法获取任何事件。在子view调用requestDisallowInterceptTouchEvent(false)后,父view才能继续拦截事件 +- 其次在ACTION_DOWN时,调用requestDisallowInterceptTouchEvent(true)来不允许父View拦截事件 +- ACTION_MOVE中如果needIntercept返回false,则调用requestDisallowInterceptTouchEvent(false)让父view重新拦截事件,需要注意的是,一点调用此方法,就表示放弃了同系列的事件的所有事件。 +- 最后调用requestDisallowInterceptTouchEvent后触发我们的onTouchEvent方法,处理时间 + + +所以父元素的拦截逻辑如下: + + @Override + public boolean onInterceptHoverEvent(MotionEvent event) { + boolean intercept = false; + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getActionMasked(); + if(action == MotionEvent.ACTION_DOWN){ + return false; + }else{ + return true + } + } + + + + +# 3 重定义dispatchTouchEvent方法 + +上面两种方法基本还是尊重系统的事件分发机制,但是还是有一些情况无法满足,这时候,我们需要根据业务需求来重新定义事件分发了。 + +比如一个下拉刷新模式 + +![](img/010_demopng.png) + +首先我们定义: +下拉刷新容器为: A +列表布局为ListView:B +刷新头为:C + +逻辑如下: +首先A或获取到事件,如果手机方向被认定为垂直滑动,A要判断C的位置和滑动方向: + +1,C刚好隐藏,此时向下滑动,B这时无法向下滑动 + +A需要拦截事件,自己处理,让C显示出来,此时A需要拦截事件,自己处理,让C显示出来,如果手指又向上滑动,则A又要判断C是否隐藏,没有隐藏还是A拦截并处理事件,当C完全隐藏后,又要吧事件交给B处理,B来实现自己列表View该有的特性 + +就这个逻辑上述方案1和方案2就无法满足,**因为系统的事件分发有一个特点**: + +- **当一个ViewGroup开始拦截并处理事件后,这个事件只能由它来处理,不可能再把事件交给它的子view处理,要么它消费事件,要么最后交给Activity的onTouchEvent处理** + +>在代码中就是,只要ViewGroup拦截了事件,他的dispatchTouchEvent方法中接收事件的子view就会被置为null, + +此特点: +- 套用到方案1外部拦截法就是,在MOVE中,开始拦截事件,View收到一个Cancel事件后,之后都无法获取到同系列事件了。 +- 套用到方案2就是在MOVE中调用requestDisallowInterceptTouchEvent(false)就表示完全放弃同系列事件的所有事件了 + + + +# 4 Demo + +说了这么方案,现在来一个实例,需求 +定义一个ViewGroup,布局方向为横向布局,可以左右滑动切换子view,同时只显示一个子view,类似ViewPager,其次ViewGroup内部放置ListView,来制造滑动冲突,我们需要解决这种冲突。 + +我们的自定义HScrollLayout代码如下: + + package com.ztiany.view.views; + + import android.content.Context; + import android.util.AttributeSet; + import android.util.Log; + import android.view.MotionEvent; + import android.view.VelocityTracker; + import android.view.View; + import android.view.ViewGroup; + import android.view.animation.AccelerateDecelerateInterpolator; + import android.widget.Scroller; + + /** + * @author Ztiany + * email 1169654504@qq.com & ztiany3@gmail.com + * date 2015年12月03日 15:23 + * description + * vsersion + */ + public class HScrollLayout extends ViewGroup { + + + public HScrollLayout(Context context) { + this(context, null); + } + + public HScrollLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public HScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public static final String TAG = HScrollLayout.class.getSimpleName(); + + private int mLastEventX, mLastEventY; + private VelocityTracker mVelocityTracker; + private Scroller mScroller; + private int mWidth; + private int mCurrentPage; + + + private void init() { + //设置方向为横向布局 + mScroller = new Scroller(getContext(), new AccelerateDecelerateInterpolator()); + mVelocityTracker = VelocityTracker.obtain(); + + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + + if (getChildCount() < 0) { + return false; + } + + + boolean intercept = false; + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + intercept = true; + } else { + //如果希望子view能接收到事件,DOWN必然要返回false + intercept = false; + mLastEventX = x; + mLastEventX = y; + } + + + break; + + case MotionEvent.ACTION_MOVE: + //计算移动差 + int dx = x - mLastEventX; + int dy = y - mLastEventY; + if (Math.abs(dx)> Math.abs(dy)) { + intercept = true; + } else { + intercept = false; + } + break; + + case MotionEvent.ACTION_UP: + //手指抬起,必须返回false,因为返回值对自己没有影响,而对子view可能有影响 + intercept = false; + break; + } + mLastEventX = x; + mLastEventY = y; + return intercept; + + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + Log.d(TAG, "l:" + l); + Log.d(TAG, "t:" + t); + Log.d(TAG, "r:" + r); + Log.d(TAG, "b:" + b); + int left = l, top = t, right = r, bottom = b; + if (changed) { + int childCount = getChildCount(); + View child; + for (int i = 0; i < childCount; i++) { + child = getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + child.layout(left, top, left + child.getMeasuredWidth(), bottom); + left += child.getMeasuredWidth(); + } + } + + } + + + @Override + public boolean onTouchEvent(MotionEvent event) { + + mVelocityTracker.addMovement(event); + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mLastEventX = x; + mLastEventX = y; + break; + + case MotionEvent.ACTION_MOVE: + int dx = x - mLastEventX; + scrollBy(-dx, 0); + + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + //将要滑动的距离 + int distanceX; + mVelocityTracker.computeCurrentVelocity(1000); + float xVelocity = mVelocityTracker.getXVelocity(); + + Log.d(TAG, "xVelocity:" + xVelocity); + + if (Math.abs(xVelocity)> 50) { + if (xVelocity> 0) {//向左 + mCurrentPage--; + } else { + mCurrentPage++; + } + + + } else { + // 不考虑加速度 + Log.d(TAG, "getScrollX():" + getScrollX()); + if (getScrollX() < 0) {//说明超出左边界 + mCurrentPage = 0; + } else { + int childCount = getChildCount(); + int maxScroll = (childCount - 1) * mWidth; + Log.d(TAG, "maxScroll:" + maxScroll); + if (getScrollX()> maxScroll) {//超出了右边界 + mCurrentPage = getChildCount() - 1; + } else { + + //在边界范围内滑动 + int currentScrollX = mCurrentPage * mWidth;//已近产生的偏移 + int offset = getScrollX() % mWidth; + Log.d(TAG, "mWidth:" + mWidth); + Log.d(TAG, "offset:" + offset); + + if (currentScrollX> Math.abs(getScrollX())) {//向左偏移 + + if (offset < (mWidth - mWidth / 3)) {//小于其 2/3 + mCurrentPage--; + } else { + + } + + } else {//向右偏移 + + if (offset> mWidth / 3) {//小于其 2/3 + mCurrentPage++; + } else { + + } + + } + + } + } + //不考虑加速度 + } + mCurrentPage = (mCurrentPage < 0) ? 0 : ((mCurrentPage> (getChildCount() - 1)) ? (getChildCount() - 1) : mCurrentPage); + distanceX = mCurrentPage * mWidth - getScrollX(); + Log.d(TAG, "distanceX:" + distanceX); + smoothScroll(distanceX, 0); + mVelocityTracker.clear(); + break; + } + mLastEventX = x; + mLastEventY = y; + //返回true,处理事件 + return true; + } + + private void smoothScroll(int distanceX, int distanceY) { + mScroller.startScroll(getScrollX(), 0, distanceX, 0, 500); + invalidate(); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + invalidate(); + } + } + + /** + * 重写测量逻辑 + * + * @param widthMeasureSpec + * @param heightMeasureSpec 这里我们不考虑wrap_content的情况,也不考虑子view的margin情况 + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + int childCount = getChildCount(); + View child; + for (int i = 0; i < childCount; i++) { + child = getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + measureChild(child, MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY)); + } + + + setMeasuredDimension(widthSize, heightSize); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mWidth = w; + } + } + + + + + +**Activity中代码如下:** + + package com.ztiany.view.activity; + + import android.graphics.Color; + import android.os.Bundle; + import android.support.v7.app.AppCompatActivity; + import android.support.v7.widget.AppCompatTextView; + import android.view.Gravity; + import android.view.View; + import android.view.ViewGroup; + import android.widget.BaseAdapter; + import android.widget.LinearLayout; + import android.widget.ListView; + import android.widget.TextView; + + import com.ztiany.view.R; + import com.ztiany.view.views.HScrollLayout; + + public class EventDemoActivity extends AppCompatActivity { + + + private HScrollLayout mHScrollLayout; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_event_deom); + initViews(); + initListView(); + } + + + private void initViews() { + + mHScrollLayout = (HScrollLayout) findViewById(R.id.id_act_event_hs); + + } + + private void initListView() { + + LinearLayout.LayoutParams lp; + ListView listView; + for (int i = 0 ; i < 3 ; i ++) { + listView = new ListView(EventDemoActivity.this); + lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + listView.setLayoutParams(lp); + listView.setAdapter(new Adapter(i)); + mHScrollLayout.addView(listView); + } + } + + + private class Adapter extends BaseAdapter { + + private final int mType; + + Adapter(int type) { + mType = type; + } + + @Override + public int getCount() { + return 100; + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + TextView textView = new AppCompatTextView(EventDemoActivity.this); + textView.setPadding(40, 40, 40, 40); + textView.setGravity(Gravity.CENTER); + convertView = textView; + if (mType == 0) { + textView.setTextColor(Color.BLUE); + } else if (mType == 1) { + textView.setTextColor(Color.RED); + + } else { + textView.setTextColor(Color.GREEN); + + } + } + TextView textView = (TextView) convertView; + textView.setText("position = " + position); + return convertView; + } + } + + + } + + + + +内部拦截法类似,稍微修改一下即可,就不贴代码了 +最后效果如下: + +![](img/010_view滑动冲突解决方案.gif) diff --git "a/Android/UI/View344円275円223円347円263円273円/011342円200円224円342円200円224円GestureDetector345円255円246円344円271円240円.md" "b/Android/UI/View344円275円223円347円263円273円/011342円200円224円342円200円224円GestureDetector345円255円246円344円271円240円.md" new file mode 100644 index 0000000..c01c44c --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/011342円200円224円342円200円224円GestureDetector345円255円246円344円271円240円.md" @@ -0,0 +1,34 @@ +##GestureDetectory +GestureDetector用来检测手势,支持很多手势操作,使用很简单,代码如下: + + mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { + + }); + //如果需要拖动,设置此方法 + mGestureDetector.setIsLongpressEnabled(false); + + + @Override + public boolean onTouchEvent(MotionEvent event) { + //事件交给mGestureDetector处理 + mGestureDetector.onTouchEvent(event); + return super.onTouchEvent(event); + } + +通知适配器SimpleOnGestureListener,我们可以监听下面所示的所有手势: +![](img/011_gestureDetetor.png) + + +这些手势说明如下: + +| 方法名 | 描述 | +| ------------ | ------------ | +| onDown | 手指轻触屏幕的一瞬间,由一个ACTION_DOWN事件触发 | +| onShowPress | 手指轻触屏幕,没有松开或者拖动,由一个ACTION_DOWN事件触发与onDown的区别是,它强调没有松开或者拖动的状态 | +| onSingleTapUp | 手指轻轻触摸屏幕后松开,伴随着一个ACTION_UP触发,这是个单击事件 | +| onScroll | 手指按下屏幕,并且拖动,有一个ACTION_DOWN和多个ACTION_MOVE触发,这是拖动行为 | +| onLongPress | 长按屏幕不放 | +| onFling | 手指按下屏幕,快速拖动屏幕后松开,有一个甩的动作 | +| onDoubleTap | 双击,由两个连续的单击事件组成 | +| onSingleTopConfirmed | 严格的单击行为,和onSingleTapUp的区别是,onSingleTopConfirmed事件,后面不可能在紧跟着一个单击行为,即这只是一个单击行为,不可能是双击行为中的一次单击,即不可能与onDoubleTap共存| +| onDoubleTapEvent | 表示发生了双击行为,在双击期间,ACTION_DOWN,ACTION_MOVE,ACTION_UP,都会触发此回调 | diff --git "a/Android/UI/View344円275円223円347円263円273円/012342円200円224円342円200円224円345円244円204円347円220円206円345円245円275円345円244円232円346円214円207円346円213円226円345円212円250円345円222円214円MotionEvent350円247円243円346円236円220円.md" "b/Android/UI/View344円275円223円347円263円273円/012342円200円224円342円200円224円345円244円204円347円220円206円345円245円275円345円244円232円346円214円207円346円213円226円345円212円250円345円222円214円MotionEvent350円247円243円346円236円220円.md" new file mode 100644 index 0000000..91355be --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/012342円200円224円342円200円224円345円244円204円347円220円206円345円245円275円345円244円232円346円214円207円346円213円226円345円212円250円345円222円214円MotionEvent350円247円243円346円236円220円.md" @@ -0,0 +1,515 @@ +# 1,简单的写一个拖动控件 + + +为了理解这个知识点,首先写一个没有处理多指拖动的DragLayout。代码很简单,就是实现一个可垂直拖动子view的布局。 + +代码如下,非常的简单。 + + + public class DragLayout extends FrameLayout { + + private static final String TAG = DragLayout.class.getSimpleName(); + private float mLastX;//手指在屏幕上最后的x位置 + private float mLastY;//手指在屏幕上最后的y位置 + private float mDownX;//手指第一次落下时的x位置(忽略) + private float mDownY;//手指第一次落下时的y位置 + + private int mScaledTouchSlop;//认为是滑动行为的最小参考值 + private boolean mIntercept;//是否拦截事件 + + public DragLayout(Context context) { + this(context, null); + } + + public DragLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + + float x = ev.getX(); + float y = ev.getY(); + int action = ev.getAction(); + + + switch (action) { + case MotionEvent.ACTION_DOWN: { + mIntercept = false; + mLastX = x; + mLastY = y; + + mDownX = x; + mDownY = y; + break; + } + + case MotionEvent.ACTION_MOVE: { + + if (!mIntercept) {//没有没有拦截,才去判断是否需要拦截 + float offset = Math.abs(mDownY - y); + Log.d(TAG, "offset:" + offset); + if (offset>= mScaledTouchSlop) { + float dx = mLastX - x; + float dy = mLastY - y; + if (Math.abs(dy)> Math.abs(dx)) { + mIntercept = true; + } + } + } + + break; + } + + case MotionEvent.ACTION_UP: { + mIntercept = false; + break; + } + } + + mLastX = x; + mLastY = y; + + return mIntercept; + } + + + @Override + public boolean onTouchEvent(MotionEvent ev) { + float x = ev.getX(); + float y = ev.getY(); + + int action = ev.getAction(); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + + mLastX = x; + mLastY = y; + + break; + } + + case MotionEvent.ACTION_MOVE: { + + float dy = mLastY - y; + scrollBy(0, (int) dy); + + break; + } + + case MotionEvent.ACTION_UP: { + break; + } + } + + mLastX = x; + mLastY = y; + + return true; + } + } + +效果图如下: + +![](img/012_bad_event.gif) + + +但是有没有发现一些不好的地方呢? +当第一个手指往下拖动了一下控件,接着第二个手指也触摸了屏幕,然后第一个离开了屏幕,这时你会看到红色的子view往上跳动了一下,这个跳动实在是太突兀了,我们希望的应该是当第一个手指离开屏幕时,红色的子view不会有任何跳动,而是依然顺畅的被第二个手指继续拖动。 + + +如下面图所示: + +![](img/012_good_event.gif) + + + + +把拖动变得顺畅需要处理多指拖动的情况,而要处理好多指拖动的情况,则需要了解MotionEvent类 + + +# 2,MotionEvent解析 + +用户在屏幕上的每个触摸行为都会被当成一个事件,而这个事件肯定有一系列属性,比如事件发送的**位置**,**类型**等等。 + +而在Android中描述这个触摸事件的就是MotionEvent,与事件分发机制相关的方法都接受一个MotionEvent对象,比如`dispatchTouchEvent`,`onInterceptTouchEvent` ,`onTouchEvent`,MotionEvent常用的属性和方法: + + + +### 1:获取事件的位置 + + getX() 获取事件的x坐标,相对于当前View + getY() 获取事件的y坐标,相对于当前View + getRawX() 获取事件的x坐标,相对于屏幕 + getRawY() 获取事件的y坐标,相对于屏幕 + + + +getX()表示获取事件相对于当前View区域的x方向的位置,那么这个值是如何得到的呢,它的处理在ViewGroup的dispatchTransformedTouchEvent方法中,父View把自身的scroll值减去子view的left,即得到事件相对于子view区域的位置。同理getY()也是如何。 + + final float offsetX = mScrollX - child.mLeft; + final float offsetY = mScrollY - child.mTop; + event.offsetLocation(offsetX, offsetY); + handled = child.dispatchTouchEvent(event); + +### 2:事件的index和pointId + + getActionIndex() 获取事件的索引值 + getPointerId(int index) 根据事件索引获取事件的id + getPointerCount() 获取触摸点的个数 + + + +由于android系统是支持多只触控的,所以在同一时刻可能会有多个触摸点,而pointerId和actionIndex就是为了区分这些触摸点。 + +- pointerId表示一个触摸点的id,它的特点是在触摸点触摸屏幕的那一刻被赋值,直到触摸点立刻屏幕之前,这个触摸点对应的pointId都不会改变。 + +- actionIndex表示一个触摸点的索引,它总是从0开始而且随着触摸点的个数而动态改变。当有两个触摸点时,其中一个触摸点的索引必然是0,另一个必然是1. + +一个MotionEvent可能会包含多个触摸点的信息。而通过pointerId和index我们可以获取不同触摸点的信息。比如: + + 首先通过getActionIndex获取触摸的索引 + getPointerId(int pointerIndex) :通过index获取触摸点的Id + findPointerIndex(int pointerId) :通过id获取触摸点的index + getX(int pointerIndex) :通过index获取对应触摸点的x坐标 + getY(int pointerIndex) :通过index获取对应触摸点的y坐标 + + + +### 3:getAction与getActionMasked + + getAction() 获取事件的类型,这是一个组合值,由pointer的index值和事件类型值组合而成的 + getActionMasked() 获取事件的类型,不具有其他信息 + + +getAction获取的是一个组合值而getActionMasked获取的值仅仅表示当前事件的类型。 + +那么这是是如何计算的呢?需要看一下源码: + + int ACTION_MASK = 0xff; //位遮罩,二进制位 1111 1111 + int ACTION_DOWN = 0; + int ACTION_UP = 1; + int ACTION_MOVE = 2; + + + //获取组合值 + public final int getAction() { + return nativeGetAction(mNativePtr); + } + //获取事件类型值 + public final int getActionMasked() { + return nativeGetAction(mNativePtr) & ACTION_MASK; + } + + int ACTION_POINTER_INDEX_MASK = 0xff00; + int ACTION_POINTER_INDEX_SHIFT = 8; + //获取事件的索引值 + public final int getActionIndex() { + return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK) +>> ACTION_POINTER_INDEX_SHIFT; + } + +getAction通过和 `ACTION_MASK `位于运算得到纯类型的值,ACTION_MASK的二进制表示为`1111 1111`,这个位于运算是为了清除Action组合值前8位的信息,由此得到事件的类型值由int值的最后8位表示。 + +再看获取`getActionIndex`的算法,通过组合值位于`ACTION_POINTER_INDEX_MASK`,再向右移动8位,所以我们可以得出结论,事件的索引值和pointId由int值的前8位表示。 + +**当只有单个触摸点时,getAction和getActionMasked获取的值是一样的** + + + + +#### 4 事件类型 + + ACTION_DOWN 表示第一个手指按下 + ACTION_UP 表示最后一个手指离开屏幕 + ACTION_MOVE 表示一个触摸点移动事件 + ACTION_POINTER_DOWN 表示一个非主要手指按下(必然已至少存在一个触摸点) + ACTION_POINTER_UP 表示一个非主要手指离开屏幕(必然至少还存在一个触摸点) + ACTION_CANCEL 表示一个事件被取消 + ACTION_OUTSIDE 表示手势操作发生在UI组件外 + +上面列举了一些常用的事件类型,而且已经注释的非常清楚了,下面对`ACTION_CANCEL`做一下特别说明。 + +ACTION_CANCEL发送的场景:比如说在一个完整的事件系列中父控件首先不拦截事件而让子view可以获取和处理事件,而在某一个时刻父控件又开始拦截事件,这时子view的事件将会被中断,以一个ACTION_CANCEL结尾。 + +可能还有一点不明白,为啥子有`ACTION_POINTER_DOWN`和`ACTION_POINTER_UP`,却没有`ACTION_POINTER_MOVE`呢? + + +这是因为考虑到触摸点移动事件发生的频率非常高,哪怕移动一小段距离也会产生很多个MOVE事件,所以为了效率和内存,安卓系统把连续的几个多触点移动事件打包到一个`MotionEvent`对象中。而前面我们也说到MotionEvent可以包含多个触摸点事件的信息。通过`getX(int)`和`getY(int)`来获得的值表示**最近发生的一个触摸点事件**的坐标。 +这时我们需要通过`getHistoricalXXX`系列方法来获取时间上稍早的触点事件的信息。 + +在[官方文档中](http://developer.android.com/intl/zh-cn/reference/android/view/MotionEvent.html)有如下一段代码,表示如何处理这种事件类型: + + void printSamples(MotionEvent ev) { + //返回此事件中的历史点数 + final int historySize = ev.getHistorySize(); + //返回事件表示的触摸点个数 + final int pointerCount = ev.getPointerCount(); + Log.d(TAG + "his", "historySize:" + historySize); + Log.d(TAG + "his", "pointerCount:" + pointerCount); + for (int h = 0; h < historySize; h++) { + Log.d(TAG + "his", "ev.getHistoricalEventTime(h):" + ev.getHistoricalEventTime(h)); + for (int p = 0; p < pointerCount; p++) { + Log.d(TAG + "his", "ev.getPointerId(p):" + ev.getPointerId(p)); + Log.d(TAG + "his", "ev.getPointerId(p):" + ev.getHistoricalX(p, h)); + Log.d(TAG + "his", "ev.getPointerId(p):" + ev.getHistoricalY(p, h)); + + } + } + Log.d(TAG, "ev.getEventTime():" + ev.getEventTime()); + for (int p = 0; p < pointerCount; p++) { + Log.d(TAG + "his", "ev.getPointerId(p):" + ev.getPointerId(p)); + Log.d(TAG + "his", "ev.getX(p): and ev.getY(p):" + ev.getX(p) + " " + ev.getY(p)); + } + } + +做了一下小改动,System.out.println()打不出log,**getHistoricalXXX只适用于MOVE事件** + +## MotionEventCompat + +使用v4包中的MotionEventCompat可以帮助我们更好的兼容各种API版本。 + + + + +# 处理多指拖动 + +分析完MotionEvent,再来思考一下如何处理多指拖动。思路是这样的: +1,一个触摸点的pointerId在离开屏幕之前是不会改变的 +2,我们在处理拖动的时候首先确认好一个pointerId,然后根据此pointerId获取对应的触摸点的位置信息,也就是我们同一时刻值处理一个触摸点 +3,当有一个新的手指按下(一个新的触摸点产生时),我们需要切换我们关心的pointerId,这是我们的处理对象就发生变化了,而此时为了防止子View的跳动,我们同时还需要更新触摸点的y值。 +4,当有一个主要的手指抬起时,我们判断这个抬起的手指是不是我们当前正在关心的那个pointerId对于的手指(触摸点),如果是我们的处理还是更新pointerId和y值。 + +代码实现如下: + + + package com.ztiany.mydemo.view; + + import android.content.Context; + import android.support.v4.view.MotionEventCompat; + import android.util.AttributeSet; + import android.util.Log; + import android.view.MotionEvent; + import android.view.ViewConfiguration; + import android.widget.FrameLayout; + + /** + * @author Ztiany + * email 1169654504@qq.com & ztiany3@gmail.com + * date 2016-03-21 15:33 + * description + * vsersion + */ + public class MultiDragLayout extends FrameLayout { + + private static final String TAG = MultiDragLayout.class.getSimpleName(); + private float mLastX; + private float mLastY; + private float mDownX;//test + private float mDownY; + + public static final int INVALID_POINTER = MotionEvent.INVALID_POINTER_ID; + private int mActivePointerId = MotionEvent.INVALID_POINTER_ID; + + private int mScaledTouchSlop; + private boolean mIntercept; + + public MultiDragLayout(Context context) { + this(context, null); + } + + public MultiDragLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public MultiDragLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + Log.d(TAG, "MultiDragLayout() called with: " + "context = [" + context + "], attrs = [" + attrs + "], defStyleAttr = [" + defStyleAttr + "]"); + init(); + } + + private void init() { + mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + Log.d(TAG, "mScaledTouchSlop:" + mScaledTouchSlop); + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + int action = MotionEventCompat.getActionMasked(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + //重置拦截标识 + mIntercept = false; + //获取初始的位置 + mLastX = ev.getX(); + mLastY = ev.getY(); + mDownX = mLastX; + mDownY = mLastY; + //这里我们根据最初的触摸的确定一个pointerId + int index = ev.getActionIndex(); + mActivePointerId = ev.getPointerId(index); + + break; + } + case MotionEventCompat.ACTION_POINTER_DOWN: { + + Log.d(TAG, "onInterceptTouchEvent() called with: " + "ev = [ACTION_POINTER_DOWN ]"); + + break; + } + case MotionEvent.ACTION_MOVE: { + //如果我们关系的pointerId==-1,不再拦截 + if (mActivePointerId == INVALID_POINTER) { + return false; + } + + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + if (pointerIndex < 0) { + return false; + } + + float currentY = MotionEventCompat.getY(ev, pointerIndex); + float currentX = MotionEventCompat.getX(ev, pointerIndex); + + if (!mIntercept) { + float offset = Math.abs(mDownY - currentY); + Log.d(TAG, "offset:" + offset); + if (offset>= mScaledTouchSlop) { + float dx = mLastX - currentX; + float dy = mLastY - currentY; + if (Math.abs(dy)> Math.abs(dx)) { + mIntercept = true; + } + + } + } + mLastX = currentX; + mLastY = currentY; + break; + } + case MotionEventCompat.ACTION_POINTER_UP: { + + Log.d(TAG, "onInterceptTouchEvent() called with: " + "ev = [ACTION_POINTER_UP ]"); + //处理手指抬起 + onSecondaryPointerUp(ev); + break; + } + case MotionEvent.ACTION_UP: { + mIntercept = false; + mActivePointerId = INVALID_POINTER; + break; + } + } + + + return mIntercept; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + int index = MotionEventCompat.getActionIndex(ev); + int pointerId = MotionEventCompat.getPointerId(ev, index); + if (mActivePointerId == pointerId) {//如果是主要的手指抬起 + final int newPointerIndex = index == 0 ? 1 : 0;//确认一个还在屏幕上手指的index + mLastY = MotionEventCompat.getY(ev, newPointerIndex);//更新lastY的值 + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);//更新pointerId + } + } + + + private void onSecondaryPointerDown(MotionEvent ev) { + final int index = MotionEventCompat.getActionIndex(ev); + mLastY = MotionEventCompat.getY(ev, index); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + } + + + @Override + public boolean onTouchEvent(MotionEvent ev) { + int action = MotionEventCompat.getActionMasked(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + + mLastX = ev.getX(); + mLastY = ev.getY(); + mDownX = mLastX; + mDownY = mLastY; + int index = ev.getActionIndex(); + mActivePointerId = ev.getPointerId(index); + + break; + } + case MotionEvent.ACTION_POINTER_DOWN: { + Log.d(TAG, "onTouchEvent() called with: " + "ev = [ACTION_POINTER_DOWN ]"); + onSecondaryPointerDown(ev); + break; + } + case MotionEvent.ACTION_MOVE: { + printSamples(ev); + int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);//主手指的索引 + if (pointerIndex < 0) { + return false; + } + + float currentX = MotionEventCompat.getX(ev, pointerIndex); + float currentY = MotionEventCompat.getY(ev, pointerIndex); + int dy = (int) (mLastY - currentY); + scrollBy(0, dy); + + mLastX = currentX; + mLastY = currentY; + break; + } + case MotionEvent.ACTION_POINTER_UP: { + Log.d(TAG, "onTouchEvent() called with: " + "ev = [ACTION_POINTER_UP ]"); + onSecondaryPointerUp(ev); + break; + } + case MotionEvent.ACTION_UP: { + mIntercept = false; + mActivePointerId = INVALID_POINTER; + break; + } + } + + + return true; + } + + //for test + void printSamples(MotionEvent ev) { + final int historySize = ev.getHistorySize(); + final int pointerCount = ev.getPointerCount(); + Log.d(TAG + "his", "historySize:" + historySize); + Log.d(TAG + "his", "pointerCount:" + pointerCount); + for (int h = 0; h < historySize; h++) { + Log.d(TAG + "his", "ev.getHistoricalEventTime(h):" + ev.getHistoricalEventTime(h)); + for (int p = 0; p < pointerCount; p++) { + Log.d(TAG + "his", "ev.getPointerId(p):" + ev.getPointerId(p)); + Log.d(TAG + "his", "ev.getPointerId(p):" + ev.getHistoricalX(p, h)); + Log.d(TAG + "his", "ev.getPointerId(p):" + ev.getHistoricalY(p, h)); + + } + } + Log.d(TAG, "ev.getEventTime():" + ev.getEventTime()); + for (int p = 0; p < pointerCount; p++) { + Log.d(TAG + "his", "ev.getPointerId(p):" + ev.getPointerId(p)); + Log.d(TAG + "his", "ev.getX(p): and ev.getY(p):" + ev.getX(p) + " " + ev.getY(p)); + } + } + + + } diff --git "a/Android/UI/View344円275円223円347円263円273円/013342円200円224円342円200円224円345円265円214円345円245円227円346円273円221円345円212円250円347円240円224円347円251円266円.md" "b/Android/UI/View344円275円223円347円263円273円/013342円200円224円342円200円224円345円265円214円345円245円227円346円273円221円345円212円250円347円240円224円347円251円266円.md" new file mode 100644 index 0000000..d1083d9 --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/013342円200円224円342円200円224円345円265円214円345円245円227円346円273円221円345円212円250円347円240円224円347円251円266円.md" @@ -0,0 +1,518 @@ +# 参考连接 +[学习Android NestedScroll](http://www.cnblogs.com/yuanchongjie/p/4981626.html) + +
+
+
+ +# 1 Android中的嵌套滑动机制 + +Android5.0开始提供嵌套滑动机制,用于给子view与父view滑动互动提供更好的交互。 +因为在原来的事件分发机制中,如果让子view开始处理事件后,父view有需要在某一个条件下处理事件,只能把子view的事件拦截,在接下来的一个完整的时间系类中,父view就无法继续给子view分发事件了,除非重写`dispatchTouchEvent`方法,但是我们知道重写这个方法还是比较有难度的。 + +在最新的V4包等兼容库中Android都对嵌套滑动提供了支持,主要类如下: + +## V4 +- NestedScrollingParent 嵌套滑动中父view接口 +- NestedScrollingParentHelper 嵌套滑动中父view接口的代理实现 +- NestedScrollingChild 嵌套滑动中子view接口 +- NestedScrollingChildHelper 嵌套滑动中子view接口的代理实现 +- NestedScrollView 支持嵌套滑动的ScrollView + +## design + +- CoordinatorLayout 协调器布局 +- CoordinatorLayout.Behavior + +## 一个注意点 + +1. 在嵌套滑动中的一些规则:子view是嵌套滑动的发起者,父view是嵌套滑动的处理者 +2. 在使用调用嵌套滑动相关的方法时,应该总是使用:ViewCompat,ViewGroupCompat, ViewParentCompat的静态方法来实现兼容 +3. 实现了NestedScrollingParent或NestedScrollingChild接口而获得的方法的实现中,应该调用final的NestedScrollingParentHelper或NestedScrollingChildHelper的对应方法来实现。 + + +
+
+
+ + +# 2 与嵌套滑动相关类的解释 + +接下来对上述的一些类进行介绍 + + +## NestedScrollingChild与NestedScrollingParent + + + public interface NestedScrollingChild { + public void setNestedScrollingEnabled(boolean enabled); + public boolean isNestedScrollingEnabled(); + public boolean startNestedScroll(int axes); + public boolean hasNestedScrollingParent(); + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); + public boolean dispatchNestedPreFling(float velocityX, float velocityY); + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); + public void stopNestedScroll(); + + + + public interface NestedScrollingParent { + + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes); + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes); + public void onStopNestedScroll(View target); + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed); + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed); + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); + public boolean onNestedPreFling(View target, float velocityX, float velocityY); + public int getNestedScrollAxes(); + + +两个接口都有对应的方法,一个需要被嵌套滑动中的父view实现,一个需要被嵌套滑动中的子view实现。 + +一个嵌套滑动的完成流程应该是这样的。 + +1. 在一个可以滑动的子view中开启嵌套滑动setNestedScrollingEnabled +2. 如果要开始一次嵌套滑动,首先应该调用startNestedScroll方法(比如在ACTION_DOWN中),通知父view开始一次嵌套滑动,方法的参数应该是ViewCompatSCROLL_AXIS_HORIZONTAL(横向)或ViewCompatSCROLL_AXIS_VERTICAL(竖向)或者他们的and/or值。这时父view的onStartNestedScroll方法将会被回调,如果父view返回true表示配合此次嵌套滑动,并且父view的onNestedScrollAccepted被调用 +3. 在子view开始滑动之前,应该先问父view许否需要先滑动,也就是调用dispatchNestedPreScroll方法,这个方法接收三个四个参数: + - dxConsumed 表示子view此次滑动期间将要消耗的水平方法的距离 + - dyConsumed 表示子view此次滑动期间将要消耗的垂直方法的距离 + - consumed 一个两个长度的数组,这个数组传递给父view,如果父view要先行滑动,将会把消耗的距离通过此数据返回给子view + - offsetInWindow 父view先完成一个滑动后子view在窗口中的偏移值。 + - 上面参数可以理解为:dxConsumed和dyConsumed是总的滑动值,传给父view,如果父view需要滑动有消耗掉一些距离,然后把消耗的距离放在consumed中,返回给子view,返回子view根据父view消耗的距离重新计算自己需要滑动的距离,进行滑动。这个过程发送在父view的onNestedPreScroll方法中。 +4. 子view在根据dispatchNestedPreScroll的返回值,然后计算被父view消耗的距离,根据需要位置 +4. 子view重新计算自己的滑动距离进行滑动之后,需要调用dispatchNestedScroll方法,此方法接收五个参数 + - int dxConsumed 子view在滑动中水平方向消耗的距离 + - int dyConsumed 子view在滑动中垂直方向消耗的距离 + - int dxUnconsumed 子view在滑动中水平方向没有消耗的距离 + - int dyUnconsumed 子view在滑动中垂直方向没有消耗的距离 + - int[] offsetInWindow 返回值。父view完成一个滑动后子view在窗口中的偏移值。 +5. 在完成一系列滑动后,如果需要停止滑动,则子view调用stopNestedScroll然后父view的onStopNestedScroll方法被回调 + + +## NestedScrollingParentHelper和NestedScrollingChildHelper分析 + +NestedScrollingParentHelper和NestedScrollingChildHelper是两个辅助类,分别对象上面分析的两个接口。系统已经给我们封装好了,我们只需要在对应的接口的方法中调用这些辅助类的实现即可。 + +### NestedScrollingChildHelper + + public class NestedScrollingChildHelper { + private final View mView;//嵌套滑动中的子view + private ViewParent mNestedScrollingParent;//嵌套滑动中的父view接口 + private boolean mIsNestedScrollingEnabled;//嵌套滑动是否可用 + private int[] mTempNestedScrollConsumed; + + public NestedScrollingChildHelper(View view) { + mView = view; + } + + //......省略一部分方法 + + public boolean startNestedScroll(int axes) { + if (hasNestedScrollingParent()) {//如果正在进行嵌套滑动,无需处理 + // Already in progress + return true; + } + if (isNestedScrollingEnabled()) {//否则如果嵌套滑动时开启的,遍历查找可以配合嵌套滑动的父view + ViewParent p = mView.getParent(); + View child = mView; + while (p != null) { + if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {//这里调用了父view的onStartNestedScroll询问是否配合嵌套滑动 + //如果配合的话,给mNestedScrollingParent赋值,再调用父view的onNestedScrollAccepted。 + mNestedScrollingParent = p; + ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); + return true;//找到了就返回 + } + if (p instanceof View) { + child = (View) p; + } + p = p.getParent(); + } + } + return false; + } + + //停止嵌套滑动,就是调用父view的onStopNestedScroll,然后mNestedScrollingParent置为null + public void stopNestedScroll() { + if (mNestedScrollingParent != null) { + ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView); + mNestedScrollingParent = null; + } + } + + + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {//判断输入值 + + /*记录子view滑动前在窗口中的位置*/ + int startX = 0; + int startY = 0; + if (offsetInWindow != null) { + mView.getLocationInWindow(offsetInWindow); + startX = offsetInWindow[0]; + startY = offsetInWindow[1]; + } + + //子view滑动后,告诉父view滑动的距离 + ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, + dyConsumed, dxUnconsumed, dyUnconsumed); + + if (offsetInWindow != null) { + //计算父view滑动后,子view在窗口中的偏移值 + mView.getLocationInWindow(offsetInWindow); + offsetInWindow[0] -= startX; + offsetInWindow[1] -= startY; + } + return true; //返回 + } else if (offsetInWindow != null) { + // No motion, no dispatch. Keep offsetInWindow up to date. + offsetInWindow[0] = 0; + offsetInWindow[1] = 0; + } + } + return false; + } + + //分发嵌套滑动,在子view开始滑动之前 + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + if (dx != 0 || dy != 0) {//判断 dx 与 dy + /*记录子view滑动前在窗口中的位置*/ + int startX = 0; + int startY = 0; + if (offsetInWindow != null) { + mView.getLocationInWindow(offsetInWindow); + startX = offsetInWindow[0]; + startY = offsetInWindow[1]; + } + + if (consumed == null) {//处理==null的情况 + if (mTempNestedScrollConsumed == null) { + mTempNestedScrollConsumed = new int[2]; + } + consumed = mTempNestedScrollConsumed; + } + consumed[0] = 0; + consumed[1] = 0; + //让父view先滑动。 + ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed); + //计算父view滑动后,子view在窗口中的偏移值 + if (offsetInWindow != null) { + mView.getLocationInWindow(offsetInWindow); + offsetInWindow[0] -= startX; + offsetInWindow[1] -= startY; + } + return consumed[0] != 0 || consumed[1] != 0;//如果父view消耗了一部分距离就返回ture + } else if (offsetInWindow != null) { + offsetInWindow[0] = 0; + offsetInWindow[1] = 0; + } + } + return false; + } + + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX, + velocityY, consumed); + } + return false; + } + + + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX, + velocityY); + } + return false; + } + + //......省略一些方法 + } + + +### NestedScrollingParentHelper + + + public class NestedScrollingParentHelper { + private final ViewGroup mViewGroup; + private int mNestedScrollAxes; + public NestedScrollingParentHelper(ViewGroup viewGroup) { + mViewGroup = viewGroup; + } + + public void onNestedScrollAccepted(View child, View target, int axes) { + mNestedScrollAxes = axes; + } + public int getNestedScrollAxes() { + return mNestedScrollAxes; + } + public void onStopNestedScroll(View target) { + mNestedScrollAxes = 0; + } + } + +NestedScrollingParentHelper就是记录NestedScrollAxes。 + +
+
+
+ + +# 实战 +我们可以可以根据嵌套滑动写一个简单的demo,效果如下: + + +![](img/013_nested_scroll.gif) + + +代码实现很简单: + + +嵌套滑动中的子view: + + + + public class NestChildView extends View implements NestedScrollingChild { + + private static final String TAG = NestChildView.class.getSimpleName(); + + private float mLastX;//手指在屏幕上最后的x位置 + private float mLastY;//手指在屏幕上最后的y位置 + + private float mDownX;//手指第一次落下时的x位置(忽略) + private float mDownY;//手指第一次落下时的y位置 + + + private int[] consumed = new int[2];//消耗的距离 + private int[] offsetInWindow = new int[2];//窗口偏移 + + + private NestedScrollingChildHelper mScrollingChildHelper; + + public NestChildView(Context context) { + this(context, null); + } + + public NestChildView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NestChildView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mScrollingChildHelper = new NestedScrollingChildHelper(this); + setNestedScrollingEnabled(true); + } + + + @Override + public boolean onTouchEvent(MotionEvent ev) { + float x = ev.getX(); + float y = ev.getY(); + + int action = ev.getAction(); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + + mDownX = x; + mDownY = y; + mLastX = x; + mLastY = y; + //当开始滑动的时候,告诉父view + startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL | ViewCompat.SCROLL_AXIS_VERTICAL); + break; + } + + case MotionEvent.ACTION_MOVE: { + /* + mDownY:293.0 + mDownX:215.0 + */ + + int dy = (int) (y - mDownY); + int dx = (int) (x - mDownX); + + //分发触屏事件给父类处理 + if (dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)) { + //减掉父类消耗的距离 + dx -= consumed[0]; + dy -= consumed[1]; + Log.d(TAG, Arrays.toString(offsetInWindow)); + } + + offsetTopAndBottom(dy); + offsetLeftAndRight(dx); + + + break; + } + + case MotionEvent.ACTION_UP: { + stopNestedScroll(); + break; + } + } + mLastX = x; + mLastY = y; + return true; + } + + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + mScrollingChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return mScrollingChildHelper.isNestedScrollingEnabled(); + + } + + @Override + public boolean startNestedScroll(int axes) { + return mScrollingChildHelper.startNestedScroll(axes); + } + + @Override + public void stopNestedScroll() { + mScrollingChildHelper.stopNestedScroll(); + + } + + @Override + public boolean hasNestedScrollingParent() { + return mScrollingChildHelper.hasNestedScrollingParent(); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); + } + + /** + * @param dx 水平滑动距离 + * @param dy 垂直滑动距离 + * @param consumed 父类消耗掉的距离 + * @return + */ + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); + } + + + } + + +嵌套滑动中的父view: + + + public class NestParentLayout extends FrameLayout implements NestedScrollingParent { + + private static final String TAG = NestParentLayout.class.getSimpleName(); + private NestedScrollingParentHelper mScrollingParentHelper; + + public NestParentLayout(Context context) { + this(context, null); + } + + public NestParentLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NestParentLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mScrollingParentHelper = new NestedScrollingParentHelper(this); + } + + + /* + 子类开始请求滑动 + */ + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + Log.d(TAG, "onStartNestedScroll() called with: " + "child = [" + child + "], target = [" + target + "], nestedScrollAxes = [" + nestedScrollAxes + "]"); + + return true; + } + + + @Override + public void onNestedScrollAccepted(View child, View target, int axes) { + mScrollingParentHelper.onNestedScrollAccepted(child, target, axes); + } + + + @Override + public int getNestedScrollAxes() { + return mScrollingParentHelper.getNestedScrollAxes(); + } + + @Override + public void onStopNestedScroll(View child) { + mScrollingParentHelper.onStopNestedScroll(child); + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + Log.d(TAG, "onNestedPreScroll() called with: " + "dx = [" + dx + "], dy = [" + dy + "], consumed = [" + Arrays.toString(consumed) + "]"); + final View child = target; + if (dx> 0) { + if (child.getRight() + dx> getWidth()) { + dx = child.getRight() + dx - getWidth();//多出来的 + offsetLeftAndRight(dx); + consumed[0] += dx;//父亲消耗 + } + + + } else { + if (child.getLeft() + dx < 0) { + dx = dx + child.getLeft(); + offsetLeftAndRight(dx); + Log.d(TAG, "dx:" + dx); + consumed[0] += dx;//父亲消耗 + } + + + } + + if (dy> 0) { + if (child.getBottom() + dy> getHeight()) { + dy = child.getBottom() + dy - getHeight(); + offsetTopAndBottom(dy); + consumed[1] += dy; + } + } else { + if (child.getTop() + dy < 0) { + dy = dy + child.getTop(); + offsetTopAndBottom(dy); + Log.d(TAG, "dy:" + dy); + consumed[1] += dy;//父亲消耗 + } + } + + + } + } diff --git "a/Android/UI/View344円275円223円347円263円273円/014342円200円224円342円200円224円Scroller-OverScroller-VelocityTracker.md" "b/Android/UI/View344円275円223円347円263円273円/014342円200円224円342円200円224円Scroller-OverScroller-VelocityTracker.md" new file mode 100644 index 0000000..6387296 --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/014342円200円224円342円200円224円Scroller-OverScroller-VelocityTracker.md" @@ -0,0 +1,538 @@ +# 内容 + + +之前已经对事件分发和MotionEvent进行了学习,但是之前的只是只能简单的实现手指拖动View的效果,而如果希望实现想ListView等可以滚动的控件的话,我们还需要学习下面的知识点。 + + +- Scroller 用于实现滑动 +- OverScroller Scroller的加强版,可以实现OverScroller +- VelocityTracker 速率跟踪器 + +


+ + + + +# 1 Scroller与VelocityTracker + +Scroller用于实现滑动和fling效果,但是其本身并没有滑动的功能,它只是帮助对需要实现的滑动效果进行计算。下面学习怎么使用Scroller。 + +### Scroll +比如一个这样的场景,当我们拖动一个View话滑动一段距离后松手,希望这个View自己滑动回到原来的位置。 + +效果: + +![](img/014_scroll.gif) + +首先我们需要实现拖动的逻辑,然后在手指松开的时候,使用Scroller计算,计算的是在指定时间内从拖动位置滑动回到原始位置的这一系列动作中,View在这个时间段内每一个时刻的位置,主要的代码如下: + + @Override + public boolean onTouchEvent(MotionEvent ev) { + int x = (int) ev.getX(); + int y = (int) ev.getY(); + + int actionMask = ev.getAction(); + + switch (actionMask) { + case MotionEvent.ACTION_DOWN: { + break; + } + case MotionEvent.ACTION_MOVE: { + int dx = mLastX - x; + int dy = mLastY - y; + scrollBy(0, dy); + mLastX = x; + mLastY = y; + break; + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + isBeginDrag = false; + scrollBack(); + break; + } + } + return true; + } + + private void scrollBack() { + mScroller.startScroll( + 0,//x的起始位置 + getScrollY(),/y的起始位置 + 0,//x方向上需要滑动的距离 + 0 - getScrollY(),//y方向上需要滑动的距离 + 300//这个滑动动作执行的时间 + ); + invalidate();//调用invalidate会导致computeScroll的执行 + } + + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) {//调用computeScrollOffset计算此时刻的位置 + //获取当前需要滑动到的位置 + int currX = mScroller.getCurrX(); + int currY = mScroller.getCurrY(); + //执行滑动 + scrollTo(0, currY); + invalidate();//反复调用invalidate,直到滑动结束 + } + } + + + + +主要的代码都在上面,需要注意的是: +- startScroll 方法的参数 +- 调用startScroll后必须调用invalidate +- 覆写computeScroll,只要computeScrollOffset方法返回true就不断的调用invalidate。 + +invalidate会导致View的重绘,而在重绘过程中computeScroll会被调用,默认它是一个空实现,主要的作用就是用来实现滑动,具体是如何调用的,可以看看源码。 + +### Fling + + 当手指快速划过屏幕,然后快速立刻屏幕时,系统会判定用户执行了一个Fling手势。这个Fling的手势应用非常广泛,比如实现类似ScrollView的快速拖动后还会惯性滑动一段距离,还有ViewPager的Fling翻页等,而实现Fling需要借助一个类`VelocityTracker`来计算用户手指滑过屏幕的速度。下面通过一个例子来学习如何实现Fling + +比如下面效果: + +![](img/014_fling.gif) + +可能效果不是很明显,但是我的手指只是快速的划了一个屏幕就松开了,剩下的惯性滑动都是依靠Scroller的fing完成的, + +主要的代码如下: + + private VelocityTracker mVelocityTracker; + private int mScaledMinimumFlingVelocity; + private int mScaledMaximumFlingVelocity; + + private void init() { + mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + mScaledMinimumFlingVelocity = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity(); + mScaledMaximumFlingVelocity = ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity(); + mVelocityTracker = VelocityTracker.obtain(); + mScroller = new Scroller(getContext()); + } + + + + @Override + public boolean onTouchEvent(MotionEvent ev) { + int x = (int) ev.getX(); + int y = (int) ev.getY(); + //把事件交给mVelocityTracker分析 + mVelocityTracker.addMovement(ev); + int actionMask = ev.getAction(); + + switch (actionMask) { + case MotionEvent.ACTION_DOWN: { + break; + } + case MotionEvent.ACTION_MOVE: { + int dx = mLastX - x; + int dy = mLastY - y; + scrollBy(0, dy); + mLastX = x; + mLastY = y; + break; + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + isBeginDrag = false; + //使用mVelocityTracker来计算速度, + //计算当前速度, 1代表px/毫秒, 1000代表px/秒, + mVelocityTracker.computeCurrentVelocity(1000,mScaledMaximumFlingVelocity); + float yVelocity = mVelocityTracker.getYVelocity(); + mVelocityTracker.clear(); + if (Math.abs(yVelocity)> mScaledMinimumFlingVelocity) { + fling(-yVelocity);//根据坐标轴正方向问题,这里需要加上-号 + } + + break; + + } + } + return true; + } + + private void fling(float v) { + mScroller.fling( + getScrollX(),//起始x位置 + getScrollY(),//起始y位置 + 0, (int) v,//x加速度,y加速度 + 0, 0,// x方向fling的范围 + 0, 1000);// y方向fling的范围 + invalidate(); + } + + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + int currX = mScroller.getCurrX(); + int currY = mScroller.getCurrY(); + scrollTo(0, currY); + invalidate(); + } + } + +可能自重最难理解的就是fling方法的后四个参数,我们就以`y方向fling的范围`来说明参数表达的具体含义。 + +- minY 表示fling的目标值不能小于的数值 +- maxY 表示fling的目标值不能超过的数值 + +比如当我的当前y是1000,然后速率是-1000,当启动一个fling后,y必然会慢慢的减少,但是y不能少于我指定的minY值。 + +比如当我的当前y是0,然后速率是1000,当启动一个fling后,y必然会慢慢的增加,但是y不能大于我指定的maxY值。 + +## VelocityTracker说明: +VelocityTracker的使用,刚刚已经贴出了相关代码, + +用VelocityTracker的静态方法`obtion`可以得到一个VelocityTracker实例,然后在一系列事件分发中不断的吧MotionEvent传递给VelocityTracker进行分析,最后通过computeCurrentVelocity方法计算x和y方向上的速率,当计算完一次速率后应该调用其clear方法清除之前的状态,而不再需要时应该调用VelocityTracker的recycler把实例放入回收池中。 + +computeCurrentVelocity(int units)说明:其中units是单位表示, 1代表px/毫秒, 1000代表1000px/秒。 + + +在使用VelocityTracker获取到速率后,应该使用ViewConfiguration的getScaledMinimumFlingVelocity方法的返回值做对比,当数据大于这个值时才应该算作是一个fling动作。 + +


+ + + + +# OverScroll + +OverScroll用于实现类似ios的滑动,在滑到边缘时依然可以滑动,松开手后自动回到开始的位置,在Android中实现这个也是比较容易的,主要涉及到的方法如下: + + +### View的overScrollBy方法 + + + protected boolean overScrollBy(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverScrollX, int maxOverScrollY, + boolean isTouchEvent) + +对参数做一下说明: + +- deltaX和deltaY 分别是需要滑动的距离 +- scrollX和scrollY 是当前的scroll值 +- scrollRangeX和scrollRangeY 标识可以滑动的范围(看下面图) +- maxOverScrollX,maxOverScrollY 表示可以overScroll的值,根据需求来设置,如果我们把maxOverScrollY设置为100,那么在滑动到上边缘后,这个View还可以继续向下滑动100,下边缘也是类似的效果。 +- isTouchEvent 表示是否是应为出触摸触发的overscroll,如果在onTouch中调用,那么久是true,如果是在computeScroll就是false。 + +![](img/014_scroll_range.png) + +### View的onOverScrolled方法 +当调用overScrollBy是,overScrollBy内部会进行一些计算然后调用onOverScrolled,而我们需要在onOverScrolled中完成内容的滑动。 + + onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {} + +- scrollX/scrollY表示需要scrollTo的x/y位置 +- clampedX/clampedY表示是否已经OverScroll到最大值,如果已经OverScroll到最大值应该调用OverScroll的springBack方法回弹到原来的位置。 + +### OverScroll的springBack方法和 + + public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) + +参数说明: +- startX/startY 当前的scroll值 +- minX/minY 传入0即可 +- maxX/maxY 传人滑动范围值 + + +### OverScroll的fling方法 +OverScroll的fling方法中有一个八个参数的重载方法,用于实现OverScroll: + + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY, int overX, int overY) { + +只是最后添加了两个参数overX和overY,这两个参数和overScrollBy方法的overScroll参数是一样的意思,就不再多说了。 + + +### EdgeEffect +EdgeEffect用于实现边缘拖动的发光效果,具体可以参考系统的ScrollView。 + + + +## Demo + + + +下面是一个例子(很多都是参考系统的ScrollView),实现了拖动OverScroll和Fling的OverScroll: + + + public class OverScrollerView extends LinearLayout { + + private static final String TAG = OverScrollerView.class.getSimpleName(); + private OverScroller mOverScroller; + private int mScaledTouchSlop; + private int mScaledMaximumFlingVelocity; + private int mScaledMinimumFlingVelocity; + + private VelocityTracker mVelocityTracker; + + private boolean mIsBeginDrag; + + private int mActivePointerId; + + private int mDownX, mDownY; + private int mLastX, mLastY; + private int mOverscrollDistance = 200; + + + public OverScrollerView(Context context) { + this(context, null); + } + + public OverScrollerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public OverScrollerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setOrientation(VERTICAL); + setOverScrollMode(OVER_SCROLL_ALWAYS); + init(); + } + + private void init() { + mOverScroller = new OverScroller(getContext()); + ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext()); + mScaledTouchSlop = viewConfiguration.getScaledTouchSlop(); + mScaledMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); + mScaledMaximumFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity(); + mOverscrollDistance = viewConfiguration.getScaledOverscrollDistance(); + mVelocityTracker = VelocityTracker.obtain(); + mOverscrollDistance = 300; + Log.d(TAG, "mOverscrollDistance:" + mOverscrollDistance); + } + + + @Override + public boolean onInterceptHoverEvent(MotionEvent event) { + + int actionMasked = MotionEventCompat.getActionMasked(event); + + if ((actionMasked == MotionEvent.ACTION_MOVE) && (mIsBeginDrag)) { + return true; + } + + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: { + if (mOverScroller.isOverScrolled()) { + mIsBeginDrag = true; + mOverScroller.abortAnimation(); + } else { + mIsBeginDrag = false; + } + int index = event.getActionIndex(); + mActivePointerId = event.getPointerId(index); + mDownX = (int) MotionEventCompat.getX(event, index); + mDownY = (int) MotionEventCompat.getY(event, index); + mLastX = mDownX; + mLastY = mDownY; + break; + } + case MotionEventCompat.ACTION_POINTER_DOWN: { + onSecondPointerDown(event); + break; + } + case MotionEvent.ACTION_MOVE: { + + if (mActivePointerId == MotionEvent.INVALID_POINTER_ID) { + return false; + } + int pointerIndex = MotionEventCompat.findPointerIndex(event, mActivePointerId); + if (pointerIndex < 0) { + return false; + } + + int currentX = (int) MotionEventCompat.getX(event, pointerIndex); + int currentY = (int) MotionEventCompat.getY(event, pointerIndex); + int dx = mLastX - currentX; + int dy = mLastY - currentY; + + if (Math.abs(dx)> mScaledTouchSlop || Math.abs(dy)> mScaledTouchSlop) { + mIsBeginDrag = true; + mLastX = currentX; + mLastY = currentY; + } + + break; + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + mIsBeginDrag = false; + break; + } + case MotionEventCompat.ACTION_POINTER_UP: { + onSecondPointerUp(event); + break; + } + } + + + return mIsBeginDrag; + } + + + @Override + public boolean onTouchEvent(MotionEvent event) { + int actionMasked = MotionEventCompat.getActionMasked(event); + mVelocityTracker.addMovement(event); + + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: { + if ((mIsBeginDrag = !mOverScroller.isFinished())) { + mOverScroller.abortAnimation(); + } + int index = event.getActionIndex(); + mActivePointerId = event.getPointerId(index); + mLastX = (int) MotionEventCompat.getX(event, index); + mLastY = (int) MotionEventCompat.getY(event, index); + break; + } + case MotionEventCompat.ACTION_POINTER_DOWN: { + onSecondPointerDown(event); + break; + } + case MotionEvent.ACTION_MOVE: { + if (mActivePointerId == MotionEvent.INVALID_POINTER_ID) { + return false; + } + int pointerIndex = MotionEventCompat.findPointerIndex(event, mActivePointerId); + if (pointerIndex < 0) { + return false; + } + int currentX = (int) MotionEventCompat.getX(event, pointerIndex); + int currentY = (int) MotionEventCompat.getY(event, pointerIndex); + int dx = mLastX - currentX; + int dy = mLastY - currentY; + if (!mIsBeginDrag && Math.abs(dx)> mScaledTouchSlop || Math.abs(dy)> mScaledTouchSlop) { + mIsBeginDrag = true; + if (dy> 0) { + dy -= mScaledTouchSlop; + } else { + dy += mScaledTouchSlop; + } + + if (dx> 0) { + dx -= mScaledTouchSlop; + } else { + dx += mScaledTouchSlop; + } + } + + if (mIsBeginDrag) { + boolean b = overScrollBy(dx, dy, getScrollX(), getScrollY(), 0, getScrollRange(), 0, mOverscrollDistance, true); + mLastX = currentX; + mLastY = currentY; + } + break; + } + case MotionEventCompat.ACTION_POINTER_UP: { + onSecondPointerUp(event); + + break; + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + mIsBeginDrag = false; + int index = MotionEventCompat.findPointerIndex(event, mActivePointerId); + mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity); + float yVelocity = mVelocityTracker.getYVelocity(index); + if (Math.abs(yVelocity)> mScaledMinimumFlingVelocity) { + Log.d(TAG, "onTouchEvent() called with: " + "doFling"); + doFling(-yVelocity); + } else if (mOverScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { + Log.d(TAG, "onTouchEvent() called with: " + "springBack"); + ViewCompat.postInvalidateOnAnimation(this); + } + mVelocityTracker.clear(); + mActivePointerId = MotionEvent.INVALID_POINTER_ID; + break; + } + } + + return true; + } + + private void doFling(float v) { + Log.d(TAG + "DD", "yVelocity:" + v); + mOverScroller.fling( + getScrollX(), + getScrollY(), + 0, (int) v, + 0, 0, + 0, getScrollRange(), + 0, mOverscrollDistance + ); + ViewCompat.postInvalidateOnAnimation(this); + } + + private void onSecondPointerDown(MotionEvent event) { + int index = MotionEventCompat.getActionIndex(event); + mActivePointerId = MotionEventCompat.getPointerId(event, index); + mLastX = (int) MotionEventCompat.getX(event, index); + mLastY = (int) MotionEventCompat.getY(event, index); + } + + + private void onSecondPointerUp(MotionEvent event) { + int index = MotionEventCompat.getActionIndex(event); + int pointerId = MotionEventCompat.getPointerId(event, index); + if (mActivePointerId == pointerId) { + int newIndex = index == 0 ? 1 : 0; + mLastX = (int) MotionEventCompat.getX(event, newIndex); + mLastY = (int) MotionEventCompat.getY(event, newIndex); + mActivePointerId = MotionEventCompat.getPointerId(event, newIndex); + } + } + + private int getScrollRange() { + int scrollRange = 0; + int childCount = getChildCount(); + if (childCount> 0) { + View child = getChildAt(childCount - 1); + scrollRange = Math.max(0, + child.getBottom() - (getHeight() - getPaddingBottom() - getPaddingTop())); + } + return scrollRange; + } + + + @Override + public void computeScroll() { + if (mOverScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mOverScroller.getCurrX(); + int y = mOverScroller.getCurrY(); + if (oldX != x || oldY != y) { + final int range = getScrollRange(); + int dx = x - oldX; + int dy = y - oldY; + overScrollBy(dx, dy, oldX, oldY, 0, range, + 0, mOverscrollDistance, false); + onScrollChanged(getScrollX(), getScrollY(), oldX, oldY); + } + + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { + Log.d(TAG, "mOverScroller.isFinished():" + mOverScroller.isFinished()); + if (!mOverScroller.isFinished()) { + super.scrollTo(scrollX, scrollY); + if (clampedX || clampedY) { + mOverScroller.springBack(this.getScrollX(), this.getScrollY(), 0, 0, 0, 0); + Log.d(TAG, "onOverScrolled-->springBack"); + } + } else { + super.scrollTo(scrollX, scrollY); + } + } + } diff --git "a/Android/UI/View344円275円223円347円263円273円/015342円200円224円342円200円224円Android347円263円273円347円273円237円347円204円246円347円202円271円.md" "b/Android/UI/View344円275円223円347円263円273円/015342円200円224円342円200円224円Android347円263円273円347円273円237円347円204円246円347円202円271円.md" new file mode 100644 index 0000000..c4963d2 --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/015342円200円224円342円200円224円Android347円263円273円347円273円237円347円204円246円347円202円271円.md" @@ -0,0 +1,29 @@ +#### Android系统的焦点 + +[官方博客](http://android-developers.blogspot.jp/2008/12/touch-mode.html) + +大多数Android设备都是触摸屏的,但是实际上Android设备也支持键盘操作,允许通过键盘来完成导航,点击,输入等。 + +**操作模式**: +- 键盘,轨迹球 +- 触摸操作 + +#### 两种模式的区别: + +**当用户处于键盘,轨迹球操作模式时**,就有必要聚焦当前的UI控件元素,例如,高亮(聚焦)某个按钮,让用户知道当前正在操作的UI元素是哪个。 + +**但是,当用户使用触摸屏与设备交互的时候**,始终聚焦当前UI元素就没有必要了,而且很丑陋;用户点击哪个元素,哪个元素就是当前元素,无需高亮标识。并且,通过触摸屏与设备交互的时候,点击某个UI元素也不会导致该元素聚焦,此时的高亮效果是由Pressed状态来完成的。也就是说,在Touch Mode模式之下,UI元素是不会进入聚焦状态的,即使调用requestFocus也不会。 + +所以为了区分这两种模式,Android定义了**Touch Mode**: + +当用户开始通过键盘与设备交互的时候,设备就退出Touch Mode模式;当用户开始通过触摸屏与设备交互的时候,设备就进入Touch Mode模式。可以通过调用View的isInTouchMode来判断设备当前是否处于Touch Mode模式。 + +#### Edittext可以在触摸模式获取焦点 +但是,也有例外情况。有些UI元素,即使是在Touch Mode的状态之下,也需要获得焦点,典型的就是Edittext。那么,这种情况该如何处理呢? + +#### 如何处理触摸模式下的焦点 + 答案就是做特殊处理。Android规定,某些元素,即使是在Touch Mode模式下,也可以获得焦点。调用View的setFocusableInTouchMode(true)可以使View在Touch Mode模式之下仍然可获得焦点(像Edittext就是在内部设置了这个属性),调用isFocusableInTouchMode可以判断View是否可在Touch Mode模式下聚焦。 + +#### 如何使用焦点 +没设置这个属性的控件在用户触摸交互时是不会获得focus的,也就是说focus在 +touch过程中是不会改变的,只是其onClickListener如果设置了的话会在up事件到来时触发。而如果设置了focusableInTouchMode属性的话,它的行为是首先尝试获得focus,如果获得成功的话其onClickListener是不会触发的,只有当你第2次再点击它时,才会执行onClickListener,所以一般情况下不推荐设置空间可以在触摸模式下获取焦点。 diff --git "a/Android/UI/View344円275円223円347円263円273円/View347円263円273円347円273円237円345円210円206円346円236円22001円342円200円224円342円200円224円View346円240円221円347円232円204円346円236円204円345円273円272円.md" "b/Android/UI/View344円275円223円347円263円273円/View347円263円273円347円273円237円345円210円206円346円236円22001円342円200円224円342円200円224円View346円240円221円347円232円204円346円236円204円345円273円272円.md" new file mode 100644 index 0000000..2737404 --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/View347円263円273円347円273円237円345円210円206円346円236円22001円342円200円224円342円200円224円View346円240円221円347円232円204円346円236円204円345円273円272円.md" @@ -0,0 +1,841 @@ +# 内容 + +- View的绘制流程简单介绍 +- setContentView的内部逻辑 +- layoutInflater解析xml布局简要分析 +- DecorView的创建过程 +- View树的结构 + +--- +


+ + + +# 1 View的绘制流程简单介绍 + +View是Android系统中很重要的一个部分: +>在Android的官方文档中是这样描述的:表示了用户界面的基本构建模块。一个View占用了屏幕上的一个矩形区域并且负责界面绘制和事件处理。 + +而Activity相当于视图层中的控制层,是用来控制和管理View的,真正用来显示和处理事件的实际上是View,当我们在Activity中调用setContentView();并传入一个View或者一个LayoutId,界面上就会显示设置的View出来,setContentView()的过程稍后分析。 + +一个view要显示在界面上,需要经历一个view树的遍历过程,这个过程又可以分为三个过程,分别是: + +- 测量 确定一个View的大小 +- 布局 确定view在父节点上的位置 +- 绘制 绘制view 的内容 + +这个过程的启动由一个叫ViewRoot(高版本中改成了ViewRootImpl)类中 performTraversals()函数发起的,子view也可以通过一些方法来请求重绘view树,但是在重绘view树时并不是所有的view都需要重新绘制,所在在view树的遍历过程中,系统会问view是否需要重新绘制,如果需要才会真的去绘制view。 + + + +这个实现在view的mPrivateFlag中, + +- View中有一个私有int变量mPrivateFlags,用于保存View的状态,int型32位,通过0/1可以保存32个状态的true或者false,采用这种方式可以有效的减少内存占用,提高运算效率,`应该可以把这种方式叫做二进制映射`。关于这个mPrivateFlags会在view的绘制流程中进行学习。 + +- 当某一个View发起了测量请求时,将会把mPrivateFlags中的某一位从0变为1,同时请求父View,父View也会把自身的该值从0变为1,同时也将会把其他子View的值从0变为1。这样一层一层传递,最终传到到DecorView,DecorView的parent是ViewRoot,所以最终都将由ViewRoot来进行处理。 + +- ViewRoot收到请求后,将会从上至下开始遍历,检查标记,只要有相对应的标记就执行测量/布局/绘制 + + + + + +流程图如下所示: + +![](img/view体系_001.jpg) + + + +--- +


+ + + + + + +# 2 View树的构建流程,从setContentView说起 + +当我们在Activity中调用setContentView();并传入一个View或者一个ViewId,界面上就会显示我们设置的View,那么这个setContentView()到底做了什么事呢?从源码中找答案。 + + +进入Activity源码可以看到如下代码: + + private Window mWindow; + + public void setContentView(@LayoutRes int layoutResID) { + getWindow().setContentView(layoutResID); + initWindowDecorActionBar(); + } + +调用的是getWindow().setContentView(layoutResID);而getrWindow返回的是mWindow,在最新的代码中可以看到mWindow是这样被初始化的: + + mWindow = new PhoneWindow(this); + +PhoneWindow是Window的子类,从Window的注释我们也可以得到PhoneWindow是Window的实现,而Window是对安卓窗口概念的抽象: + + /** + * Abstract base class for a top-level window look and behavior policy. An + * instance of this class should be used as the top-level view added to the + * window manager. It provides standard UI policies such as a background, title + * area, default key processing, etc. + * + *

The only existing implementation of this abstract class is + * android.policy.PhoneWindow, which you should instantiate when needing a + * Window. Eventually that class will be refactored and a factory method + * added for creating Window instances without knowing about a particular + * implementation. + */ + public abstract class Window {......} + +一个抽象的基础的顶级窗口类,定义的基本的行为和外观政策,这类的实例应该用作顶层视图添加到窗口管理器,它提供了标准UI政策背景等标题区域,默认键处理等。 + +继续跟踪PhoneWindow,下面是PhoneWindow的源码部分,可以看到其内部定义了平时开发中用到的各种元素。 + + /** + * Android-specific Window. + *

+ * todo: need to pull the generic functionality out into a base class + * in android.widget. + */ + public class PhoneWindow extends Window implements MenuBuilder.Callback { + ...... + private boolean mIsFloating; + + private LayoutInflater mLayoutInflater; + + private TextView mTitleView; + ...... + + } + + + +查看setContentView的源码实现:如果是view则会为其设置一个ViewGroup的LayoutParams,宽高都为匹配父元素,添加到mContentParent中,如果是布局id,先用布局填充器来解析布局id指定的xml文件,然后添加到mContentParent中,那mContentParent怎么初始化的呢?很明显是在installDecor()中被初始化的。 + + @Override + public void setContentView(int layoutResID) { + if (mContentParent == null) { + installDecor();//安装decorView + } else { + mContentParent.removeAllViews();//如果已经安装过,就移除之前的conent,重新设置 + } + mLayoutInflater.inflate(layoutResID, mContentParent); + final Callback cb = getCallback(); + if (cb != null) { + cb.onContentChanged(); + } + } + + @Override + public void setContentView(View view) { + setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + if (mContentParent == null) { + installDecor(); + } else { + mContentParent.removeAllViews(); + } + mContentParent.addView(view, params); + final Callback cb = getCallback(); + if (cb != null) { + cb.onContentChanged(); + } + } + +接下来先看布局填充器如何解析xml布局 + +## 2.1 布局填充器LayoutInflate解析xml布局分析 + +首先mLayoutInflate是这样被初始化的 + + mLayoutInflater = LayoutInflater.from(context); + +查看LayoutInfater源码: + + + /** + * 这个类用来实例化xml中定义的view节点, + * 使用Activity#getLayoutInflater()或者 Context#getSystemService}获取一个标准的LayoutInflater实例 + + */ + public abstract class LayoutInflater { + + ...... + + /** + 比如通过from方法获取一个LayoutInflater实例 + * Obtains the LayoutInflater from the given context. + */ + public static LayoutInflater from(Context context) { + LayoutInflater LayoutInflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + if (LayoutInflater == null) { + throw new AssertionError("LayoutInflater not found."); + } + return LayoutInflater; + } + + ...... + + + } + +可以看到可以看到LayoutInflater是个抽象类,LayoutInflater使用pull解析的方式来解析xml,而它的继承者是PhoneLayoutInflater,可以在Policy类中找到相关构建过程。 + + +### 具体的解析xml 布局流程: + + +其具体的解析xml布局实现为下面代码,下面贴出了所有相关的方法 + + + public View inflate(int resource, ViewGroup root, boolean attachToRoot) { + if (DEBUG) System.out.println("INFLATING from resource: " + resource); + //xml构建一个xml解析器 + XmlResourceParser parser = getContext().getResources().getLayout(resource); + try { + return inflate(parser, root, attachToRoot); + } finally { + parser.close(); + } + } + + + /** + * Inflate a new view hierarchy from the specified XML node. Throws + * 从指定的节点inflate一个新的view层 + */ + public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { + synchronized (mConstructorArgs) {//解析需要同步,(同步可能导致低效,代码构建view层优于xml解析) + final AttributeSet attrs = Xml.asAttributeSet(parser);//解析属性 + /** + private final Object[] mConstructorArgs = new Object[2]; + */ + Context lastContext = (Context)mConstructorArgs[0]; + mConstructorArgs[0] = mContext; + View result = root;//定义根节点view,首先指向root + try { + // Look for the root node. + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG && + type != XmlPullParser.END_DOCUMENT) { + // Empty + } + + if (type != XmlPullParser.START_TAG) { + throw new InflateException(parser.getPositionDescription() + + ": No start tag found!"); + } + + final String name = parser.getName(); + + //这里处理了merge节点 + if (TAG_MERGE.equals(name)) { + if (root == null || !attachToRoot) {//可以看到merge的使用条件 + throw new InflateException(" can be used only with a valid " + + "ViewGroup root and attachToRoot=true"); + } + //解析merge节点 + rInflate(parser, root, attrs); + } else { + // Temp is the root view that was found in the xml + View temp = createViewFromTag(name, attrs);//从tag创建view + + //构建LayoutParams + ViewGroup.LayoutParams params = null; + if (root != null) { + // Create layout params that match root, if supplied + params = root.generateLayoutParams(attrs);//从我们指定的root构建LayoutParams + if (!attachToRoot) { + // Set the layout params for temp if we are not + // attaching. (If we are, we use addView, below) + temp.setLayoutParams(params); + } + } + // Inflate all children under temp,解析子节点 + rInflate(parser, temp, attrs); + //关于attach参数的处理 + if (root != null && attachToRoot) { + root.addView(temp, params); + } + //关于返回值的处理 + if (root == null || !attachToRoot) { + result = temp; + } + } + + } catch (XmlPullParserException e) { + InflateException ex = new InflateException(e.getMessage()); + ex.initCause(e); + throw ex; + } catch (IOException e) { + InflateException ex = new InflateException( + parser.getPositionDescription() + + ": " + e.getMessage()); + ex.initCause(e); + throw ex; + } finally { + // Don't retain static reference on context. + mConstructorArgs[0] = lastContext; + mConstructorArgs[1] = null; + } + return result; + } + } + + /** + * 根据指定的名称,前缀,属性创建一个view + * @return View The newly instantied view, or null. + */ + public final View createView(String name, String prefix, AttributeSet attrs) + throws ClassNotFoundException, InflateException { + Constructor constructor = sConstructorMap.get(name); + Class clazz = null; + try { + if (constructor == null) {//从构造器的缓存中获取构造器 + clazz = mContext.getClassLoader().loadClass( + prefix != null ? (prefix + name) : name); + + if (mFilter != null && clazz != null) { + boolean allowed = mFilter.onLoadClass(clazz); + if (!allowed) { + failNotAllowed(name, prefix, attrs); + } + } + constructor = clazz.getConstructor(mConstructorSignature); + sConstructorMap.put(name, constructor);//添加到容器中 + } else { + // If we have a filter, apply it to cached constructor + if (mFilter != null) { + // Have we seen this name before? + Boolean allowedState = mFilterMap.get(name); + if (allowedState == null) { + // New class -- remember whether it is allowed + clazz = mContext.getClassLoader().loadClass( + prefix != null ? (prefix + name) : name); + + boolean allowed = clazz != null && mFilter.onLoadClass(clazz); + mFilterMap.put(name, allowed); + if (!allowed) { + failNotAllowed(name, prefix, attrs); + } + } else if (allowedState.equals(Boolean.FALSE)) { + failNotAllowed(name, prefix, attrs); + } + } + } + + Object[] args = mConstructorArgs; + args[1] = attrs; + return (View) constructor.newInstance(args); + + } catch (NoSuchMethodException e) { + InflateException ie = new InflateException(attrs.getPositionDescription() + + ": Error inflating class " + + (prefix != null ? (prefix + name) : name)); + ie.initCause(e); + throw ie; + + } catch (ClassNotFoundException e) { + // If loadClass fails, we should propagate the exception. + throw e; + } catch (Exception e) { + InflateException ie = new InflateException(attrs.getPositionDescription() + + ": Error inflating class " + + (clazz == null ? "" : clazz.getName())); + ie.initCause(e); + throw ie; + } + } + + + + /** + * This routine is responsible for creating the correct subclass of View + * given the xml element name. Override it to handle custom view objects. If + * you override this in your subclass be sure to call through to + * super.onCreateView(name) for names you do not recognize. + * + 可以看到这里调用了createView,并传入"android.view."前缀,其实就是Android系统View的包名了,所以我们可能 + 在xml中只写系统view的类名 + */ + protected View onCreateView(String name, AttributeSet attrs) + throws ClassNotFoundException { + return createView(name, "android.view.", attrs); + } + + /* + * 默认构建view的方法 + */ + View createViewFromTag(String name, AttributeSet attrs) { + if (name.equals("view")) { + name = attrs.getAttributeValue(null, "class"); + } + + try { + View view = (mFactory == null) ? null : mFactory.onCreateView(name, + mContext, attrs); + + if (view == null) {//判断是否是系统级别view + if (-1 == name.indexOf('.')) { + view = onCreateView(name, attrs); + } else {//自定义view需要写全路径名 + view = createView(name, null, attrs); + } + } + + return view; + + } catch (InflateException e) { + throw e; + + } catch (ClassNotFoundException e) { + InflateException ie = new InflateException(attrs.getPositionDescription() + + ": Error inflating class " + name); + ie.initCause(e); + throw ie; + + } catch (Exception e) { + InflateException ie = new InflateException(attrs.getPositionDescription() + + ": Error inflating class " + name); + ie.initCause(e); + throw ie; + } + } + + /** + * 这里是用递归的方法构建view的所有层级。当view从xml中初始化它的所有子view之后,会调用onFinishInflate()方法 + */ + private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs) + throws XmlPullParserException, IOException { + + final int depth = parser.getDepth(); + int type; + + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth()> depth) && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + final String name = parser.getName(); + + if (TAG_REQUEST_FOCUS.equals(name)) { + parseRequestFocus(parser, parent); + } else if (TAG_INCLUDE.equals(name)) { + if (parser.getDepth() == 0) { + throw new InflateException(" cannot be the root element"); + } + parseInclude(parser, parent, attrs); + } else if (TAG_MERGE.equals(name)) { + throw new InflateException(" must be the root element"); + } else { + final View view = createViewFromTag(name, attrs); + final ViewGroup viewGroup = (ViewGroup) parent; + final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); + rInflate(parser, view, attrs); + viewGroup.addView(view, params); + } + } + + parent.onFinishInflate(); + } + + private void parseRequestFocus(XmlPullParser parser, View parent) + throws XmlPullParserException, IOException { + int type; + parent.requestFocus(); + final int currentDepth = parser.getDepth(); + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth()> currentDepth) && type != XmlPullParser.END_DOCUMENT) { + // Empty + } + } + + //处理include节点 + private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs) + throws XmlPullParserException, IOException { + + int type; + + if (parent instanceof ViewGroup) { + final int layout = attrs.getAttributeResourceValue(null, "layout", 0); + if (layout == 0) { + final String value = attrs.getAttributeValue(null, "layout"); + if (value == null) { + throw new InflateException("You must specifiy a layout in the" + + " include tag: "); + } else { + throw new InflateException("You must specifiy a valid layout " + + "reference. The layout ID " + value + " is not valid."); + } + } else { + final XmlResourceParser childParser = + getContext().getResources().getLayout(layout); + + try { + final AttributeSet childAttrs = Xml.asAttributeSet(childParser); + + while ((type = childParser.next()) != XmlPullParser.START_TAG && + type != XmlPullParser.END_DOCUMENT) { + // Empty. + } + + if (type != XmlPullParser.START_TAG) { + throw new InflateException(childParser.getPositionDescription() + + ": No start tag found!"); + } + + final String childName = childParser.getName(); + + if (TAG_MERGE.equals(childName)) { + // Inflate all children. + rInflate(childParser, parent, childAttrs); + } else { + ViewGroup.LayoutParams params = null; + try { + params = group.generateLayoutParams(attrs); + } catch (RuntimeException e) { + params = group.generateLayoutParams(childAttrs); + } finally { + if (params != null) { + view.setLayoutParams(params); + } + } + + // Inflate all children. + rInflate(childParser, view, childAttrs); + TypedArray a = mContext.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.View, 0, 0); + int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID); + // While we're at it, let's try to override android:visibility. + int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1); + a.recycle(); + + if (id != View.NO_ID) { + view.setId(id); + } + + switch (visibility) { + case 0: + view.setVisibility(View.VISIBLE); + break; + case 1: + view.setVisibility(View.INVISIBLE); + break; + case 2: + view.setVisibility(View.GONE); + break; + } + + group.addView(view); + } + } finally { + childParser.close(); + } + } + } else { + throw new InflateException(" can only be used inside of a ViewGroup"); + } + + final int currentDepth = parser.getDepth(); + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth()> currentDepth) && type != XmlPullParser.END_DOCUMENT) { + // Empty + } + } + +首先其解析xml的方式是pull解析,inflate方法的三个参数都非常重要,稍后分析。 +方法的开发是对xml规范的一些判断,不符合直接抛异常,如对merge的处理是父view必须非null而且必须添加到父view中去: + + if (TAG_MERGE.equals(name)) { + if (root == null || !attachToRoot) { + throw new InflateException(" can be used only with a valid " + + "ViewGroup root and attachToRoot=true"); + } + + + +接下来看一个view的初始化,是通过` View temp = createViewFromTag(name, attrs);` +方法继续初始化的,关联方法是createView,从其内部逻辑看出,xml中的View都是通过反射进行实例化的 + + +下面是关于inflate方法三个参数的逻辑: + View temp = createViewFromTag(name, attrs); + + ViewGroup.LayoutParams params = null; + + if (root != null) { + params = root.generateLayoutParams(attrs); + if (!attachToRoot) { + temp.setLayoutParams(params); + } + } + + +- 第一步:如果root 非null + +>ViewGroup.LayoutParams会被初始化,调用的是`root.generateLayoutParams(attrs)` +如果attachToRoot为false,LayoutParams设置给被创建的view。 + +- 第二步:如果root 非null 并且attachToRoot为true + +>LayoutParams设置给被创建的view,并且被创建的view添加到root总作为子view + +- 第三步:如果root 为null 并且attachToRoot为false + +>则只是单纯的创建一个View了 + +**有一点需要注意的是LayoutInflater的返回值问题,如果root 非null 并且attachToRoot为true返回的View是root,否则为为inflate的根View。** + +**到这里应该可以明白为何有时候,在平时的inflate方法调用时,如果root传空的话,根布局的layout_width等属性都是无效的!!!** + +当一个节点调用完毕,又会调用` rInflate(parser, temp, attrs);`进行递归解析 + +在rInflate函数中还可以看到 `parent.onFinishInflate();`函数的调用时机。 + + +以上基本就是xml布局解析的过程,更多具体细节可以参考源码。 + + +## 2.2 installDecor()的源码实现分析 + +源码如下: + + private void installDecor() { + //初始化decorView + if (mDecor == null) { + mDecor = generateDecor(); + mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + mDecor.setIsRootNamespace(true); + } + //初始化ContentParent + if (mContentParent == null) { + mContentParent = generateLayout(mDecor); + //查找title + mTitleView = (TextView)findViewById(com.android.internal.R.id.title); + if (mTitleView != null) { + //FEATURE_NO_TITLE的处理 + if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) { + View titleContainer = findViewById(com.android.internal.R.id.title_container); + if (titleContainer != null) { + titleContainer.setVisibility(View.GONE); + } else { + mTitleView.setVisibility(View.GONE); + } + if (mContentParent instanceof FrameLayout) { + ((FrameLayout)mContentParent).setForeground(null); + } + } else { + mTitleView.setText(mTitle); + } + } + } + } + + +可以看到如果mDecor为null的话会调用`generateDecor`初始化一个DecorView,代码如下: + + protected DecorView generateDecor() { + return new DecorView(getContext(), -1); + } + + +代码很简单就是直接创建了一个DecorView,那么来看看DecorView的实现: + + + + private final class DecorView extends FrameLayout implements RootViewSurfaceTaker{ + ...... + } + + +DecorView是PhoneWindow的内部类,并且是final的,集成自FrameLayout,其实DecorView是每一个View树的跟布局。 + +接下来还有一个重点:mContentView的初始化 + +`mContentParent = generateLayout(mDecor);` + +根据一系列的风格判断,最终确定ContentView的布局id + +比如mIsFloating,Window_windowNoTitle等等... + +确定好id之后会进行解析,然后添加到decorView中,最后初始化mContentView + + View in = mLayoutInflater.inflate(layoutResource, null); + decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); + + +ID_ANDROID_CONTENT的值为:`com.android.internal.R.id.content;` + + +上面说的根据一系列窗口特点和系统风格确定好布局id,style是在xml的theme中指定,那么怎么改变窗口的特征呢? + +其实就是requestFeature这个方法了,但是其内部有一段代码是这样的: + + if (mContentParent != null) { + throw new AndroidRuntimeException("requestFeature() must be called before adding content"); + } +也就是说,requestFeature必须在setContentView之前调用。 + + +分析到这里也可以明白,有写时候不希望界面显示title,我们会调用: + + getWindow().requestFeature(Window.FEATURE_NO_TITLE); + +其实原理就是在构建view树的时候,根据设置的窗口特性,去解析不同的布局xml + + +比如一般解析的是:screen_title + +```xml + + + + + + +``` + + +如果没有title则会解析screen_simple + +```xml + + +``` + + + +**到此View树的创建分析完毕。** + + +### 2.3 总结 + +- Activity被创建后,会调用Activity的onCreate方法。我们通过设置setContentView就会调用到Window中的setContextView,从而初始化DecorView。 + +- 我们需要隐藏标题栏什么的,都需要在DecorView初始化之前进行设置。 + + +


+ + + +# 3 View的mPrivaeFlag简要说明 + +刚刚说到,view的重新绘制过程中并不是需要绘制view树种的所有view,这由view的一些flag决定, + + +例如view的draw方法中: + + public void draw(Canvas canvas) { + final int privateFlags = mPrivateFlags; + //判断是否需要绘制,View的是否绘制由mPrivateFlags中一位标识, + final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && + (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); + //重置 + mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; + + int saveCount; + + if (!dirtyOpaque) { + drawBackground(canvas); + } + + // skip step 2 & 5 if possible (common case) + final int viewFlags = mViewFlags; + boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; + boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; + if (!verticalEdges && !horizontalEdges) { + // Step 3, draw the content + if (!dirtyOpaque) onDraw(canvas); + + // Step 4, draw the children + dispatchDraw(canvas); + + // Overlay is part of the content and draws beneath Foreground + if (mOverlay != null && !mOverlay.isEmpty()) { + mOverlay.getOverlayView().dispatchDraw(canvas); + } + + // Step 6, draw decorations (foreground, scrollbars) + onDrawForeground(canvas); + + // we're done... + return; + } + ......} + + + +还有:一般情况下viewGroup的ondraw方法是不会被调用,因为没有可以绘制的内容,这时它就是透明的状态,对于不透明的计算条件有一个方法computeOpaqueFlags: + + protected void computeOpaqueFlags() { + // Opaque if:不透明条件 + // - Has a background + // - Background is opaque + // - Doesn't have scrollbars or scrollbars overlay + + if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) { + mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND; + } else { + mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND; + } + + final int flags = mViewFlags; + if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) || + (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY || + (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) { + mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS; + } else { + mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS; + } + } + + + + +还有一个方法setWillNotDraw,其实也是对mPrivateFlat进行这种运算。 + + + + /** + * If this view doesn't do any drawing on its own, set this flag to + * allow further optimizations. By default, this flag is not set on + * View, but could be set on some View subclasses such as ViewGroup. + * + * Typically, if you override {@link #onDraw(android.graphics.Canvas)} + * you should clear this flag. + * + * @param willNotDraw whether or not this View draw on its own + */ + public void setWillNotDraw(boolean willNotDraw) { + setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); + } diff --git "a/Android/UI/View344円275円223円347円263円273円/View347円263円273円347円273円237円345円210円206円346円236円22002円342円200円224円342円200円224円View346円240円221円351円201円215円345円216円206円347円232円204円345円274円200円345円247円213円.md" "b/Android/UI/View344円275円223円347円263円273円/View347円263円273円347円273円237円345円210円206円346円236円22002円342円200円224円342円200円224円View346円240円221円351円201円215円345円216円206円347円232円204円345円274円200円345円247円213円.md" new file mode 100644 index 0000000..b731734 --- /dev/null +++ "b/Android/UI/View344円275円223円347円263円273円/View347円263円273円347円273円237円345円210円206円346円236円22002円342円200円224円342円200円224円View346円240円221円351円201円215円345円216円206円347円232円204円345円274円200円345円247円213円.md" @@ -0,0 +1,645 @@ +#内容 + + +内容: + +- invalidate 方法分析 +- requestLayout方法分析 +- rootView初始化及与DecorView进行管理,并添加到WindowManager分析 +- performTraversals简要分析 +- Activity-ViewRoot-Window-WindowManager关系简要分析 + + +--- +


+ + + +# 1 View的绘制流程-什么时候发起绘制以及发起的绘制流程分析 + + +前面说到在PhoneWindow中,调用setContentView,然后解析xml布局(如果传参的xmlId的话),从而完成整个View树的创建 + +但是只是创建了View树,并没有执行View树的遍历操作,也就不会执行测量,布局,绘制等操作,这样视图还是无法显示出来,那么View树的遍历是从哪里开始的呢? + +前面说过是在ViewRoot.java类中 performTraversals()函数发起的,那么这个函数又是被谁调用的呢?下面开始分析 + + +## 1.1 引起View树重新绘制的方 +首先能够引起View树重新绘制的方法有: + +- 1,invalidate()方法 : + + 请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程, + 并且只绘制那些"需要重绘的"视图,即谁(View的话,只绘制该View ;ViewGroup, + 则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。 + + 一般引起invalidate()操作的函数如下: + - 1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。 + - 2、setSelection()方法 :请求重新draw(),但只会绘制调用者本身。 + - 3、setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法, 继而绘制该View。 + - 4 、setEnabled()方法 : 请求重新draw(),但不会重新绘制任何视图包括该调用者本身。 + +- 2,requestLayout()方法 : + + 会导致调用measure()过程 和 layout()过程 。 + 说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,不会重新绘制任何视图包括该调用者本身。 + + 一般引起requestLayout()操作的函数如setVisibility()方法,当View的可视状态在INVISIBLE/ VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法,同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要"重新绘制"的视图。 + +- 3,requestFocus()方法: + +请求View树的draw()过程,但只绘制"需要重绘"的视图。 + +具体可以参考《安卓内核剖析:第十三章View工作原理》 + + + + +接下来分析这些方法: + +invalidate方法用的比较多,当一个View的内容放生变化时,我们就会调用这个方法,然后其View的onDraw方法就会被调用,从而重新绘制View的内容,那么其内部的调用过程是怎样的呢? + +## 1.2 invalidate方法分析 + + +invalidate方法在View中实现如下: + + //只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对整个View + public void invalidate() { + invalidate(true); + } + //default的权限,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对整个View + void invalidate(boolean invalidateCache) { + invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true); + } + + + //只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对局部View + public void invalidate(int l, int t, int r, int b){......} + + //实质还是调运invalidateInternal方法 + void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, + boolean fullInvalidate) { + ...... + + if (skipInvalidate()) {//是否跳过Invalidate + return; + } + + if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) + || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) + || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED + || (fullInvalidate && isOpaque() != mLastIsOpaque)) { + if (fullInvalidate) { + mLastIsOpaque = isOpaque(); + mPrivateFlags &= ~PFLAG_DRAWN; + } + //这里修改了PFLAG_DIRTY标志 + mPrivateFlags |= PFLAG_DIRTY; + + + // 计算重绘的区域,然后调用父节点的invalidateChild方法 + final AttachInfo ai = mAttachInfo; + final ViewParent p = mParent; + if (p != null && ai != null && l < r && t < b) { + final Rect damage = ai.mTmpInvalRect; + //设置刷新区域 + damage.set(l, t, r, b); + //调用父View的invalidateChild方法 + p.invalidateChild(this, damage); + } + ...... + } + + +需要注意的是skipInvalidate()方法:如果满足下面方法条件,就会导致invalidate方法无效 + + /** + * 不可见的或者是没有执行动画的view或者没有Transitioning将不会被绘制,这些view将不会被设置ditry_flag + */ + private boolean skipInvalidate() { + return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null && + (!(mParent instanceof ViewGroup) || + !((ViewGroup) mParent).isViewTransitioning(this)); + } + + +上面分析总结如下: + +将要刷新区域直接传递给了父ViewGroup的invalidateChild方法,在invalidate中,调用父View的invalidateChild,这是一个从当前向上级父View不断传递的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集 。所以我们看下ViewGroup的invalidateChild方法,源码如下: + + + public final void invalidateChild(View child, final Rect dirty) { + ViewParent parent = this; + + ......省略代码 + + do { + ......省略代码 + //invalidateChildInParent返回当前View的mParent + parent = parent.invalidateChildInParent(location, dirty); + ......省略代码 + } while (parent != null); + } + } + +这个过程就是不断的向上调用parent.invalidateChildInParent(location, dirty)方法。那么最终这个方法会向上执行到哪呢,我们需要分析View的parent是怎么赋值的: + +首先View的mParent是这样被赋值的? + + + void assignParent(ViewParent parent) { + if (mParent == null) { + mParent = parent; + } else if (parent == null) { + mParent = null; + } else { + throw new RuntimeException("view " + this + " being added, but" + + " it already has a parent"); + } + } + + +assignParent是在什么时候被调用的? + +1,当一个View被添加到一个ViewGroup时: + + public void addView(View child, int index, LayoutParams params) { + ...... + + // addViewInner() will call child.requestLayout() when setting the new LayoutParams + // therefore, we call requestLayout() on ourselves before, so that the child's request + // will be blocked at our level + requestLayout(); + invalidate(true); + addViewInner(child, index, params, false); + } + + +addViewInner + + private void addViewInner(View child, int index, LayoutParams params, + boolean preventRequestLayout) { + + ...... + + // tell our children + if (preventRequestLayout) { + child.assignParent(this); + } + + } + +2,但是Decor作为一个View树的跟布局,肯定不可能被添加到ViewGroup中,那么它的mParent是谁呢?答案是ViewRoot(ViewRootImpl),看下面分析。 + + +## 1.3 DecorView的mParent被赋值过程、ViewRoot被创建过程与添加到WindowManager简单分析 + +现在的问题是ViewRoot是什么时候被赋值的? + +熟悉Activity生命周期方法的都知道,Activity有如下方法: + +onCreate +onStart +onResume +onPause +onStop +onDestory + +其中onCreate表示Activity被创建,我们也在这里setContentView,onStart表示视图即将可见,onResume表示当前Activity已可以与用户进行交互,并且视图已经可见,所以可以从这里开始分析, + +熟悉Activity架构的都应该知道,Activity的生命周期方法是在ActivityManagerService通过ApplicationThread进行调用的,ApplicationThread通过H类发送消息到ActivityThread,进行Activity的各生命周期方法的操作与回调 + +onCreate表示Activity被创建,此时DecorView压根就没被创建,直接略过 + + +onStart分析:代码中也没有相关逻辑 + +onResume方法:直接看handleResumeActivity + + final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) { + ...... + if (r.window == null && !a.mFinished && willBeVisible) { + r.window = r.activity.getWindow(); + View decor = r.window.getDecorView(); + decor.setVisibility(View.INVISIBLE); + ViewManager wm = a.getWindowManager(); + WindowManager.LayoutParams l = r.window.getAttributes(); + a.mDecor = decor; + l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; + l.softInputMode |= forwardBit; + if (a.mVisibleFromClient) { + a.mWindowAdded = true; + wm.addView(decor, l); + } + + ...... + } + +可以看到有` wm.addView(decor, l);`这样一段逻辑,这个wm其实就是WindowManager,其实现类是WindowManagerImpl,addView的具体实现为: + + private void addView(View view, ViewGroup.LayoutParams params, boolean nest) + { + if (Config.LOGV) Log.v("WindowManager", "addView view=" + view); + + if (!(params instanceof WindowManager.LayoutParams)) { + throw new IllegalArgumentException( + "Params must be WindowManager.LayoutParams"); + } + + final WindowManager.LayoutParams wparams + = (WindowManager.LayoutParams)params; + + ViewRoot root; + View panelParentView = null; + + synchronized (this) { + // notification gets updated. + int index = findViewLocked(view, false); + if (index>= 0) { + if (!nest) { + throw new IllegalStateException("View " + view + + " has already been added to the window manager."); + } + root = mRoots[index]; + root.mAddNesting++; + // Update layout parameters. + view.setLayoutParams(wparams); + root.setLayoutParams(wparams, true); + return; + } + + if (wparams.type>= WindowManager.LayoutParams.FIRST_SUB_WINDOW && + wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) { + final int count = mViews != null ? mViews.length : 0; + for (int i=0; i +--- +**这里说一下ViewRoot的创建过程**: + +首先从ViewRoot的构造函数说起: + + public ViewRoot(Context context) { + super(); + + if (MEASURE_LATENCY && lt == null) { + lt = new LatencyTimer(100, 1000); + } + + // For debug only + //++sInstanceCount; + + // Initialize the statics when this class is first instantiated. This is + // done here instead of in the static block because Zygote does not + // allow the spawning of threads. + getWindowSession(context.getMainLooper()); + + mThread = Thread.currentThread(); + mLocation = new WindowLeaked(null); + mLocation.fillInStackTrace(); + mWidth = -1; + mHeight = -1; + mDirty = new Rect(); + mTempRect = new Rect(); + mVisRect = new Rect(); + mWinFrame = new Rect(); + //初始化W, + mWindow = new W(this, context); + mInputMethodCallback = new InputMethodCallback(this); + mViewVisibility = View.GONE; + mTransparentRegion = new Region(); + mPreviousTransparentRegion = new Region(); + mFirst = true; // true for the first time the view is added + mAdded = false; + //初始化mAttachInfo sWindowSession是WindowManager服务的远程引用 + mAttachInfo = new View.AttachInfo(sWindowSession, mWindow, this, this); + mViewConfiguration = ViewConfiguration.get(context); + mDensity = context.getResources().getDisplayMetrics().densityDpi; + } + +构造函数初始化了一些对象, + +- W是一个本地的Bidner,将会传递给WindowManagerService, +- AttachInfo ,表示一组View的信息,当一个View附加到一个Window上后,View的attachInfo被赋值 +- sWindowSession是WindowManager服务的远程引用 + +然后是add + + res = sWindowSession.add(mWindow, mWindowAttributes, + getHostVisibility(), mAttachInfo.mContentInsets, + mInputChannel); + + + +这里让客户端的mWindow与服务端的WidowManagerService产生关联,mWindow就是W,是一个Binder结构,传递给服务端后,服务端就可以主动调用客户端,这样也是双方都掌握着主动调用的跨进程通信方式 +
+
+ +--- + + +### postInvalidate + + postInvalidate方法与invalidate方法类似。只是它适合于在子线程调用。 + +###requestLayout方法分析: + +和invalidate类似,其实在上面分析View绘制流程时或多或少都调运到了这个方法,而且这个方法对于View来说也比较重要,所以我们接下来分析一下他。如下View的requestLayout源码: + + public void requestLayout() { + ...... + if (mParent != null && !mParent.isLayoutRequested()) { + //由此向ViewParent请求布局 + //从这个View开始向上一直requestLayout,最终到达ViewRootImpl的requestLayout + mParent.requestLayout(); + } + ...... + } + +其本质也是向上层层传递,直到ViewRootImpl为止,然后触发ViewRootImpl的requestLayout方法,如下就是ViewRoot的requestLayout方法: + + public void requestLayout() { + checkThread(); + mLayoutRequested = true; + scheduleTraversals(); + } + + +**至此View的绘制流程,什么时候发起绘制,以及发起绘制的流程分析完毕。** + + + +总结: + +1,View的一个简单架构图: + +![](img/view体系_002.png) + + + +2,ViewRoot与ViewGroup都实现了ViewParent接口,ViewParent主要提供了一系列操作子View的方法例如焦点的切换,显示区域的控制等等。 + +3,ViewGroup和WindowManager都实现了ViewManager接口,ViewManager提供了三个抽象方法addView,removeView,updateViewLayout。用来添加、删除、更新布局。 + +可见ViewGroup作为一个View的容器,有添加删除子view的功能,也有控制子view焦点等功能,而ViewRoot则只需要控制子view焦点等功能,因为它不需要去控制子view的删除等操作,这都是decorView和其子容器的事,而WindowManager作为一个窗口当然也会有添加,删除view的功能,而控制子view焦点等功能则通过viewRoot去实现。 + +从以上可得,利用接口把复杂的逻辑按照职责区分,子类按照自己的职责任务去实现不同的接口,而在逻辑调用时,只需要通过接口去声明,偶尔性降低,职责明确后代码也很清晰,这就是所谓的面向接口编程吧!!! + + + + +--- +
+ +## 导致View树重新遍历的时机: +上面笔记说了: +- requestFocus +- invalidate +- requestLayout +都会导致View树的重新遍历,那么内部是什么机制呢?接着分析: + +遍历View树意味着整个View需要重新对其包含的子视图分配大小并重绘。一般情况下导致遍历原因有三个: +- View本身内部状态发生变化,而引起重绘 +- view树内部添加了或者删除了子view +- View本身的大小及可见性发生了变化 + +具体可以分为: +- View的状态发生变化 StateListDrawable +- refreshDrawableList +- onFocusedChanged +- serVisibility +- setEnable +- setSelected +- invalidate +- requestLayout +- requestFocused + + +具体可以参考《安卓内核剖析》十三章 View的工作原理 + + +--- +
+
+
+ +# 2 View的绘制起点-performTraversals方法分析: + + +performTraversals方法太过复杂,具体的逻辑可以去查看源码,大概的流程如下: + +### dispatchAttachedToWindow + +判断是否是第一次初始化,如果是则做一些初始化操作 + +第一次attach到Window,通知所有子view,传递attachInfo对象 +` host.dispatchAttachedToWindow(attachInfo, 0);` + +### 测量 + +判断是否需要重新测量,需要则执行测量 + + + /** + lp的定义:`WindowManager.LayoutParams lp = mWindowAttributes;` + */ + //初始化根View的测量规格 + childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); + childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height) + //执行测量,后期的版本可能是preformMeasure + host.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + + +这里可以看一下测量规格产生的方法:一般都是屏幕的宽高 + + private int getRootMeasureSpec(int windowSize, int rootDimension) { + int measureSpec; + switch (rootDimension) { + + case ViewGroup.LayoutParams.MATCH_PARENT: + // Window can't resize. Force root view to be windowSize. + //窗口不能调整大小,使用窗口的size + measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); + break; + case ViewGroup.LayoutParams.WRAP_CONTENT: + // Window can resize. Set max size for root view. + //窗口可以调整大小,使用最大的size + measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); + break; + default: + // Window wants to be an exact size. Force root view to be that size. + //窗口想要一个精确的大小,使用窗口的参数 + measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); + break; + } + return measureSpec; + } + + + + +### layout + +判断是否需要重新布局,需要则重新布局 + + host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight); + + + +### draw + +判断是否重新绘制,调用draw方法 + + mView.draw(canvas); + + +从这个流程也可以看到, + +draw方法中,canvas的初始化,当然GL11是很复杂的东西,暂时不研究: + + final GL11 gl = (GL11) context.getGL(); + mGL = gl; + mGlCanvas = new Canvas(gl); + + +View树的布局完毕通知也是在遍历中通知的: + + if (triggerGlobalLayoutListener) { + attachInfo.mRecomputeGlobalAttributes = false; + attachInfo.mTreeObserver.dispatchOnGlobalLayout(); + } + + + +差不多就是这样子了。水平有限,更多细节可以参考《Android内核剖析》 + + + +## 3 总结 + + + + +- DecorView初始化之后将会被添加到WindowManager中,同时WindowManager中会为新添加的DecorView创建一个对应的ViewRoot,并把DecorView设置给ViewRoot。所以view树的根View就是DecorView,因为DecorView的父亲是ViewRoot,实现了ViewParent接口,但是没有继承自View,所以根本不是一个View,它可以理解为View树的管理者,其成员变量mView作为它管理的View树的根View,遍历流程由它发起,ViewRoo它的核心任务是与WindowManagerService进行通信。 + +- 当Activity被创建时,会相应的创建一个Window对象,Window对象创建时会获取应用的WindowManager(注意,这是应用的窗口管理者,不是系统的,是LocalWindowManager,不过其内部还是持有系统WindowManager的引用),WindowManger继承自ViewManager,而添加到WindowManager中的是DecorView,不是Window,所以其实真正意义上的window就是View。 + + ViewManager的定义很简单就是添加、更新,删除view: + + public interface ViewManager{ + public void addView(View view, ViewGroup.LayoutParams params); + public void updateViewLayout(View view, ViewGroup.LayoutParams params); + public void removeView(View view); + } + 而WindowManager的实现了ViewManager,并添加了对窗口管理的一系列行为与属性,从而简化了客户端对窗口的操作。 + + +- 当ViewRoot的setView方法中将会调用requestLayout进行第一次视图测量请求。同时sWindowSession.add自身内部的W对象,以此达到和WindowManagerService的关联。ViewRoot在ViewRoot的构造方法中会通过getWindowSession来获取WindowManagerService系统服务的远程对象 + + +- Activity可以看做UI管理者,但它不直接管理View树和ViewRoot,它内部有一个Window对象,其实例是PhoneWindow,Activity通过PhoneWindow构建View树,通过对Window的风格设置来控制View树构建,Window字面意思就是窗口,而Window是一个抽象的概念,根据不同的产品可以有不同的实现,具体由Activity.attact中调用PolicyManager.makeNewWindow决定的。 + + + +ViewRoot在各个版本的不同名称: +![](img/view体系_ViewRoot.png) + + + + + +、 diff --git "a/Android/UI/View344円275円223円347円263円273円/img/001_view345円235円220円346円240円207円.png" "b/Android/UI/View344円275円223円347円263円273円/img/001_view345円235円220円346円240円207円.png" new file mode 100644 index 0000000..5bd5fb7 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/001_view345円235円220円346円240円207円.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/002_layoutParams.png" "b/Android/UI/View344円275円223円347円263円273円/img/002_layoutParams.png" new file mode 100644 index 0000000..099f6c4 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/002_layoutParams.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/002_346円265円213円351円207円217円346円265円201円347円250円213円.png" "b/Android/UI/View344円275円223円347円263円273円/img/002_346円265円213円351円207円217円346円265円201円347円250円213円.png" new file mode 100644 index 0000000..2e9c45e Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/002_346円265円213円351円207円217円346円265円201円347円250円213円.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/002_346円265円213円351円207円217円350円247円204円346円240円274円.png" "b/Android/UI/View344円275円223円347円263円273円/img/002_346円265円213円351円207円217円350円247円204円346円240円274円.png" new file mode 100644 index 0000000..92dbee7 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/002_346円265円213円351円207円217円350円247円204円346円240円274円.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/002_351円201円215円345円216円206円350円277円207円347円250円213円.jpg" "b/Android/UI/View344円275円223円347円263円273円/img/002_351円201円215円345円216円206円350円277円207円347円250円213円.jpg" new file mode 100644 index 0000000..a7c3e6b Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/002_351円201円215円345円216円206円350円277円207円347円250円213円.jpg" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/008_345円220円214円345円220円221円345円206円262円347円252円201円.png" "b/Android/UI/View344円275円223円347円263円273円/img/008_345円220円214円345円220円221円345円206円262円347円252円201円.png" new file mode 100644 index 0000000..0953cd0 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/008_345円220円214円345円220円221円345円206円262円347円252円201円.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/008_345円244円215円346円235円202円345円206円262円347円252円201円.png" "b/Android/UI/View344円275円223円347円263円273円/img/008_345円244円215円346円235円202円345円206円262円347円252円201円.png" new file mode 100644 index 0000000..78e32b5 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/008_345円244円215円346円235円202円345円206円262円347円252円201円.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/008_345円267円246円345円217円263円344円270円212円344円270円213円345円206円262円347円252円201円.png" "b/Android/UI/View344円275円223円347円263円273円/img/008_345円267円246円345円217円263円344円270円212円344円270円213円345円206円262円347円252円201円.png" new file mode 100644 index 0000000..ddf1bde Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/008_345円267円246円345円217円263円344円270円212円344円270円213円345円206円262円347円252円201円.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/009_scroller.jpg" "b/Android/UI/View344円275円223円347円263円273円/img/009_scroller.jpg" new file mode 100644 index 0000000..31024dc Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/009_scroller.jpg" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/010_demopng.png" "b/Android/UI/View344円275円223円347円263円273円/img/010_demopng.png" new file mode 100644 index 0000000..df393a6 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/010_demopng.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/010_view346円273円221円345円212円250円345円206円262円347円252円201円350円247円243円345円206円263円346円226円271円346円241円210円.gif" "b/Android/UI/View344円275円223円347円263円273円/img/010_view346円273円221円345円212円250円345円206円262円347円252円201円350円247円243円345円206円263円346円226円271円346円241円210円.gif" new file mode 100644 index 0000000..830af99 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/010_view346円273円221円345円212円250円345円206円262円347円252円201円350円247円243円345円206円263円346円226円271円346円241円210円.gif" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/011_gestureDetetor.png" "b/Android/UI/View344円275円223円347円263円273円/img/011_gestureDetetor.png" new file mode 100644 index 0000000..9070eeb Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/011_gestureDetetor.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/012_bad_event.gif" "b/Android/UI/View344円275円223円347円263円273円/img/012_bad_event.gif" new file mode 100644 index 0000000..f74a729 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/012_bad_event.gif" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/012_good_event.gif" "b/Android/UI/View344円275円223円347円263円273円/img/012_good_event.gif" new file mode 100644 index 0000000..d2dafe6 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/012_good_event.gif" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/013_nested_scroll.gif" "b/Android/UI/View344円275円223円347円263円273円/img/013_nested_scroll.gif" new file mode 100644 index 0000000..badaea3 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/013_nested_scroll.gif" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/014_fling.gif" "b/Android/UI/View344円275円223円347円263円273円/img/014_fling.gif" new file mode 100644 index 0000000..6a52a98 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/014_fling.gif" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/014_scroll.gif" "b/Android/UI/View344円275円223円347円263円273円/img/014_scroll.gif" new file mode 100644 index 0000000..b69a391 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/014_scroll.gif" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/014_scroll_range.png" "b/Android/UI/View344円275円223円347円263円273円/img/014_scroll_range.png" new file mode 100644 index 0000000..d9e54e9 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/014_scroll_range.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/04_layout346円265円201円347円250円213円.png" "b/Android/UI/View344円275円223円347円263円273円/img/04_layout346円265円201円347円250円213円.png" new file mode 100644 index 0000000..2cb0ba8 Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/04_layout346円265円201円347円250円213円.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/view344円275円223円347円263円273円_001.jpg" "b/Android/UI/View344円275円223円347円263円273円/img/view344円275円223円347円263円273円_001.jpg" new file mode 100644 index 0000000..a7c3e6b Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/view344円275円223円347円263円273円_001.jpg" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/view344円275円223円347円263円273円_002.png" "b/Android/UI/View344円275円223円347円263円273円/img/view344円275円223円347円263円273円_002.png" new file mode 100644 index 0000000..99a6fbb Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/view344円275円223円347円263円273円_002.png" differ diff --git "a/Android/UI/View344円275円223円347円263円273円/img/view344円275円223円347円263円273円_ViewRoot.png" "b/Android/UI/View344円275円223円347円263円273円/img/view344円275円223円347円263円273円_ViewRoot.png" new file mode 100644 index 0000000..80ee75b Binary files /dev/null and "b/Android/UI/View344円275円223円347円263円273円/img/view344円275円223円347円263円273円_ViewRoot.png" differ diff --git a/Java/EffectiveJava/README.MD b/Java/EffectiveJava/README.MD new file mode 100644 index 0000000..28b8c3f --- /dev/null +++ b/Java/EffectiveJava/README.MD @@ -0,0 +1,9 @@ +# EffectiveJava读书笔记 + +这是我阅读EffectiveJava做的一些笔记,首先想说的是EffectiveJava真的是Java里面的经典书籍,而对于经典书籍只读一遍是不可能理解其深意的,笔记的目的在于记录一些重要的概念,很多东西并没有深入研究,在这里推荐一个别人写的EffectiveJava学习笔记,作者写的非常好也非常详细,不仅仅只是做了比较,而且还加上了自己的理解和代码实践,有兴趣的朋友可以看一下: + +[跟我一起学EffectiveJava系列](http://www.cnblogs.com/JohnTsai/tag/Java/) + + + +# 目录 diff --git "a/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/001_cpu345円237円272円347円241円200円347円237円245円350円257円206円.MD" "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/001_cpu345円237円272円347円241円200円347円237円245円350円257円206円.MD" index 4ec59f2..21a5aa6 100644 --- "a/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/001_cpu345円237円272円347円241円200円347円237円245円350円257円206円.MD" +++ "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/001_cpu345円237円272円347円241円200円347円237円245円350円257円206円.MD" @@ -1,3 +1,5 @@ +# 内容 + 参考 - 《Java并发编程的艺术》 - 《深入理解计算机系统》 @@ -5,7 +7,16 @@ - [基于CAS操作的非阻塞算法](http://www.cnblogs.com/ktgu/p/3529145.html) -# 一些基本概念 +内容包括: + +- 上下文切换,cpu的基本知识与硬件组成 +- 高速缓存,总线 +- 程序,线程,进程 +- 一些基本的cpu术语与解释 + + + +# 1 一些基本概念 在学习并发编程之前先了解一些概念 @@ -68,7 +79,7 @@ CPU围绕着主存,寄存器文件,算术/逻辑单元进行的,CPU在指 java代码在编译后会变成java字节码,字节码被类加载其加载到jmv里,jvm执行字节码,最终需要转化为汇编指令在cpu上执行,java中并发机制依赖于jvm的实现和cpu指令。了解cpu的相关术语有助于后面的学习。 -# cpu的一些内存术语 +# 2 cpu的一些内存术语 术语|描述 ---|--- @@ -79,27 +90,3 @@ java代码在编译后会变成java字节码,字节码被类加载其加载到 缓存命中(cache hit)|如果cpu进行高速缓存行填充操作的缓存位置仍然是下次处理器操访问的地址时,处理器从缓存中读取数据,而不是内存中 写命中(write hit)|当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓冲行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中 写缺失(write misses the cache)|一个有效的缓存行被写入到不存在的内存区域 - - - - - - - - - - - - - - - - - - - - - - - - diff --git "a/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/002_Java345円206円205円345円255円230円346円250円241円345円236円213円.MD" "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/002_Java345円206円205円345円255円230円346円250円241円345円236円213円.MD" index f7e71cf..3d90d96 100644 --- "a/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/002_Java345円206円205円345円255円230円346円250円241円345円236円213円.MD" +++ "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/002_Java345円206円205円345円255円230円346円250円241円345円236円213円.MD" @@ -1,3 +1,10 @@ +# 内容 +- 什么是内存模型 +- Java内存模型的定义 +- Java内存模型的抽象结构,内存分区(共享,本地),数据的操作规定,保证可见性 +- 指令重排序,内存屏障,happens before,as if serial语义,数据依赖,重排序对程序的影响 +- 顺序一致性的内存模型,数据竞争,JMM与顺序一致性的内存模型的区别,临界区,总线事务(读事务/写事务) + # 1 内存模型 ## 1.1 什么是内存模型 @@ -20,7 +27,9 @@ Java内存模型简称JMM,而JMM指的就是一套规范,现在最新的规 2. **线程之间通过什么方式通信才合法,才能得到期望的结果**。 -并发编程模型的两个关键问题:线程之间如何`通信`及线程之间如何`同步`。通信是指线程之间以何种方式来交换信息。命令编程模式下主要有两种通信机制:`共享内存`和`消息传递`。同步是指程序中用于控制不同线程间操作发生相对顺序机制。Java并发采用的是`共享内存模式`。 +并发编程模型的两个关键问题:线程之间如何`通信`及线程之间如何`同步`。 +- 通信是指线程之间以何种方式来交换信息。命令编程模式下主要有两种通信机制:`共享内存`和`消息传递`。Java并发采用的是`共享内存模式`。 +- 同步是指程序中用于控制不同线程间操作发生相对顺序机制。 ## 1.3 Java内存模型的抽象结构 @@ -50,12 +59,13 @@ java线程之间的通信方式有JMM控制,JMM决定一个线程对共享变 } 一个线程间通信的过程需要经历两个步骤: -1. 线程A把本地内存中修改的共享变量i刷新到主内存中去。按顺序细分为下面三个步骤: - - 读取主内存中的i,保存i的副本到本地内存中 - - 修改本地内存中i的值 - - 把i的值刷新到主内存中去 -2. 线程B到主内存中去读取线程A之间更新过的共享变量。 +1. 本地内存A和本地内存B由主内存中共享变量x的副本 +2. 线程A把本地内存中修改的共享变量i刷新到主内存中去。按顺序细分为下面三个步骤: + - 修改本地内存中i的值,存到在本地内存 + - 当需要通讯的时候,把i的值刷新到主内存中去 +3. 线程B到主内存中去读取线程A之间更新过的共享变量。 +从整体角度生来,这两个步骤实质就是线程A向线程B发生消息,而这个过程必须通过主内存。 JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性的保证。 @@ -70,15 +80,7 @@ JMM通过控制主内存与每个线程的本地内存之间的交互,来为Ja - 指令级并行的重排序,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 - 内存系统重排序,由于处理器可以使用缓存和读写缓冲区,这使得加载和存储操作看起来可能是乱序执行的。 -```flow -st=>start: 源代码 -e=>end: 最终执行结果 -op1=>operation: 编译器优化重排序 -op2=>operation: 指令级重排序 -op3=>operation: 内存重排序 -st->op1->op2->op3 -op3->e -``` +![](img/002_reorder.png) 这些重排序可能会导致多线程出现的内存可见性问题。 @@ -125,7 +127,7 @@ happens-before就是什么一定发生在什么之前,jsr133采用happens-befo 与程序员密切相关的happens-before规则如下: * 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。 -* 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。 +* 监视器锁规则:(一个线程)对一个监视器锁的解锁,happens- before 于随后对(另一个线程)这个监视器锁的加锁。 * volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。 * 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。 @@ -166,7 +168,7 @@ happens-before就是什么一定发生在什么之前,jsr133采用happens-befo ## 2.4 as-if-serial语义与程序顺序规则 -as-if-serial语义是指,遍历器和处理器为了提高并行度时可以对某些执行进行重排序,但是不管怎么排序,(单线程)程序的执行结果不能被改变。编译器和处理器,rutime都必须遵守ai-if-serial语义 +as-if-serial语义是指,编译器和处理器为了提高并行度时可以对某些执行进行重排序,但是不管怎么排序,(单线程)程序的执行结果不能被改变。编译器和处理器,rutime都必须遵守ai-if-serial语义 如果有下面三个步骤: @@ -235,3 +237,97 @@ as-if-serial语义是指,遍历器和处理器为了提高并行度时可以 由此可见,**重排序对多线程并发操作共享变量会产生不可预估的影响。** + + + + +# 3.3 顺序一致性 +顺序一致性的内存模型是一个**理论的参考模型**,再设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型为参照 + +## 3.1 数据竞争 +java内存模型对数据竞争的规范如下: +- 在一个线程中写一个变量 +- 在另一个线程中读取同一个变量 +- 而且写和读没有通过同步来排序 + + +如果一个多线程的程序正确同步,那么就不存在数据竞争的问题,如果程序是正确同步的,程序的执行结果将具有顺序一致性,即程序的执行结果与顺序一致性内存模型的执行结果相同。 + +这里的同步是指(volatile synchronized fianl)的正确使用 + +## 3.2 顺序一致性内存模型 +- 一个线程的所有操作必须按照程序的顺序来执行 +- 不管程序是否同步,所有的线程都执行看到一个单一的操作执行顺序,每一个操作都必须原子执行且立刻对所有的线程可见 + +这就是理论的顺序一致性内存模型 + +但是顺序一致性内存模型只是一个参考的内存模型,而JVM根本不保证这样的顺序一致性,不但不保证顺序一致性,而且所有线程看到的操作顺序也可能不一致。 + +- 对于一个正确同步的程序,它的执行结果具有顺序一致性 + +下面是一个正确同步的程序: + + class SynchronizedExample { + int a = 0; + boolean flag = false; + + public synchronized void writer() { + a = 1; + flag = true; + } + + public synchronized void reader() { + if (flag) { + int i = a; + } + } + } + +这个程序的执行具有顺序一致性的执行结果 + +虽然JMM**允许在临界区被进行内存重排序**(synchronized内),但是JMM不允许临界区内的代码溢出到临界区外。 + + +- 对于未同步或者为正确同步的程序,JMM只保证最新的安全性,JMM保证线程执行时读取到的值,要么是某个线程写入的值,要么是初始化的值,而不会无中生有的冒出来。JMM不会保证未同步或者未正确同步的程序的顺序一致性,因为未同步或者未正确同步的程序整体上是无须的,无法预估其执行结果。 + +为什么说整体上是无须的呢,类似下面代码: + + new Thread(new Runnable() {//A + @Override + public void run() { + volatileExample.writer(); + } + }).start(); + + + new Thread(new Runnable() {//B + @Override + public void run() { + volatileExample.reader(); + } + }).start(); +对于这段没用同步的代码,无法保证线程A线程B的执行顺序,A/B的执行是随机的,顺序都无法保证,所以无法保证顺序一致性。 + + + +### JMM与顺序一致性内存模型的差异 +- JMM不保证单个线程的操作会按照程序顺序执行,比如重排序 +- 顺序一致性保证所有线程都能看到一致的操作结果,而JMM不保证 +- JMM不保证对64位的long、double、变量的写操作具有原子性,而顺序一致性内存模型保证所有的内存读写都具有原子性 + +>64位 CPU,是指CPU内部的通用寄存器的宽度为64比特,支持整数的64比特宽度的算术与逻辑运算。 + +### 总线事务 +数据通过总线在处理器和内存间进行传递,每一次处理器和内存之间的数据传输都是通过一些列操作来完成的,这一系列步骤称之为**总线事务** + +总线事务包括: +- 读事务 数据从内存读取到处理器 +- 写事务 数据从处理器写入到内存 + +关键点在于:总线会同步试着视图并发使用总线的事务,当一个处理器在总线的事务期间,总线会禁止其他处理器和I/O设备执行内存的读/写 + +在任意的时刻,最多只能有一个处理器访问内存,总线的这个特性确保了**单个总线事务之中的内存读/内存写操作具有原子性** + +>在一些32位的处理器上,如果要求对64位数据的写操作具有原子性,会有比较大的开销,JMM鼓励但不要求JVM对64为数据的血操作具有原子性。 + +需要注意的是,在JSR133之前(JDK5),一个64为位的数据的读写可以被拆分成两个32位的读写操作,但是在JSR133之后,只允许对64数据的写操作拆分成两个32位的写操作,任意数据的去操作都具有原子性。 diff --git "a/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/003_volatile347円232円204円345円206円205円345円255円230円350円257円255円344円271円211円.md" "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/003_volatile347円232円204円345円206円205円345円255円230円350円257円255円344円271円211円.md" new file mode 100644 index 0000000..9186efd --- /dev/null +++ "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/003_volatile347円232円204円345円206円205円345円255円230円350円257円255円344円271円211円.md" @@ -0,0 +1,178 @@ + +#1 volatile的内存语义 +当声明变量为volatile后,对这个变量的读/写将会很特别,为了深入理解volatile实现的原理,接下来学习volatile的内存语义和volatile的内存语义的实现 + +## 1.1 volatile的特性 + +理解volatile的特性的一个好办法是把对volatile变量的单个读/写,看成是使用同一个所对这些单个读/写操作做了同步。例如: + +volatile代码实例 + + public class VolatileFeatureExample { + + volatile long v1 = 0L;//声明volatile的变量 + + public void set(long l) {//volatile 的set + v1 = l; + } + + public void getAndIncrement() {//复合volatile的读/写 + v1++; + } + + public long get() {//volatile的读 + return v1; + } + } + + +上面代码示例等同于下面同步后的代码 + + class VolatileFeatureExample2 { + + long v1 = 0L;//声明普通变量 + + public synchronized void set(long l) {//同步的写 + v1 = l; + } + + public void getAndIncrement() { + long temp = get();//同步的读 + temp += 1L;//普通的写 + set(temp);//同步写 + } + + public synchronized long get() {//同步的写 + return v1; + } + } + + +锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着:**对一个volatile变量的写,总是能看到(任意线程)对这个volatile变量最后的写入。** + +而锁的临界区的执行具有原子性,意味着即使是对64位数据的变量类型,只要他们是volatile的,对该变量的读/写都具有原子性,**但是多个volatile变量的操作,类似volatile++这种复合操作,不具有原子性**。 + +所以总得volatile变量自身具有如下特性: +- 可见性 对一个volatile变量的读,总是可以看到任意线程对这个volatile变量的写 +- 原子性 对任意单个volatile变量的读/写都具有原子性,但类似于volatile++这种复合的volatile操作,不具有原子性 + +## 1.2 volatile写-读建立的关系 + +从JSR-133(JDK5)开始,volatile变量的写-读可以实现线程之间的通信,从内存语义来讲,volatile的的写读操作与所的释放-获取具有相同的效果,即volatile的写与锁的释放具有相同的语义,volatile的都与锁的获取具有相同的语义。如: + + + class VolatileExample { + int a = 0; + boolean flag = false; + + public void writer() { + a = 1; //步骤1 + flag = true; //步骤2 + } + + + public void reader() { + + if (flag) { //步骤3 + int i = a; //步骤4 + System.out.println(i); + } + } + } + + + new Thread(new Runnable() {//A + @Override + public void run() { + volatileExample.writer(); + } + }).start(); + + + new Thread(new Runnable() {//B + @Override + public void run() { + volatileExample.reader(); + } + }).start(); + + +*`如果`* **线程A执行writer后线程B执行reader**,根据happens befor规则,这个过程建立的happens bofore关系如下: + +- 1 happens bofore 2 +- 3 happens bofore 4 +- 根据volatile的内存语义,2 happens bofore 3 (读 before 写) +- 根据 happens bofore的传递性,1 happens bofore 4 + + + + +## 1.3 volatile写-读内存语义 +** volatile写的内存语义**: +当写一个volatile变量时,JMM会把该线程对应本地内存中的共享变量刷新到主内存中去。 +** volatile读的内存语义**: +当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取该变量。 +用线程间的通信的说法就是,线程A写一个volatile变量,随后线程B读取这个volatile变量,这个过程实质上是线程A通过主内存向线程B发现消息。 + +## 1.4 volatile内存语义的实现 + +为了实现volatile的内存语言,JMM会分别限制两种类型的重排序类型,限制的重排序类型如下,NO表示禁止的重排序。 + +| 第一个操作\第二个操作 | 普通读写 | volatile读 | volatile写 | +| ------------ | ------------ | ------------ | ------------ | +| 普通读写 | | | NO | +| volatile读 | NO | NO | NO | +| volatile写 | | NO | NO | + +举例说明,第一行第三列的NO表示,如果第一个操作是普通的读/写操作,而第二个操作是volatile写操作的话,JMM就会进程这两个操作的重排序。 + +JMM采取的方式是: + +- 在每个volatile写操作前面插入一个StoreStore内存屏障 +- 在每个volatile写操作后面插入一个StoreLoad内存屏障 +- 在每个volatile读操作前面插入一个LoadLoad内存屏障 +- 在每个volatile读操作后面插入一个LoadStore内存屏障 + +JMM采取的策略是保存策略——首先保证正确性,再去追求执行效率 + +## 1.5 JSR-133为什么要增强volatile的内存语义 +在JSR-133之前,JMM虽然不允许volatile变量之间的重排序,但是运行volatile变量与普通变量重排序。 + +旧的内存模型中程序的操作可能被重排序成下列时序执行: + +![](img/003_volatile_old.png) + + +在旧的JMM中,当1和2之间没有数据依赖时,1和2之间的操作就可能被重排序,3和4的执行结果是:线程B执行4时,不一定能看到线程A在执行1时对共享变量的修改。 + +用代码来说明就是: + + class VolatileExample { + int a = 0; + boolean flag = false; + + public void writer() { + a = 1; //步骤1 + flag = true; //步骤2 + } + + + public void reader() { + + if (flag) { //步骤3 + int i = a; //步骤4 + System.out.println(i); + } + } + } + +线程A执行writer,然后线程B执行reader,由于1和2可以被重排序,所以线程B在执行4时,可能看不到线程A对变量a的修改。 + + + +在旧的JMM中,volatile的写都没有锁释放和锁获取操作所具有的内存语义,为了提供一种比锁更轻量级的线程之间的通信机制,JSR-133专家们决定之前对volatile的内存语义。 + + + + +>对volatile的学习就到这里了,这里只是对《java并发编程的艺术》重点部分的记录,如果对java并发编程感谢的话,强烈推荐这本书。 diff --git "a/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/README.MD" "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/README.MD" index abc1c24..1083b1e 100644 --- "a/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/README.MD" +++ "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/README.MD" @@ -1,3 +1,4 @@ -- [Java并发编程学习001——cpu基础知识](001_cpu基础知识.MD) -- [Java并发编程学习002——Java内存模型-未完](002_Java内存模型.MD) +- [Java并发编程的艺术001——cpu基础知识](001_cpu基础知识.MD) +- [Java并发编程的艺术002——Java内存模型](002_Java内存模型.MD) +- [Java并发编程的艺术003——volatile的内存语义](003_volatile的内存语义.md) diff --git "a/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/img/002_reorder.png" "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/img/002_reorder.png" new file mode 100644 index 0000000..05300d0 Binary files /dev/null and "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/img/002_reorder.png" differ diff --git "a/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/img/003_volatile_old.png" "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/img/003_volatile_old.png" new file mode 100644 index 0000000..a7601c3 Binary files /dev/null and "b/Java/Java345円271円266円345円217円221円347円274円226円347円250円213円347円232円204円350円211円272円346円234円257円/img/003_volatile_old.png" differ diff --git a/README.MD b/README.MD index 1bf8a1e..98a7f7e 100644 --- a/README.MD +++ b/README.MD @@ -1,6 +1,5 @@ -我是[Ztiany](http://weibo.com/u/1854760051?refer_flag=1005055010_&is_all=1),这是我的博客。我会在这里慢慢分享我所有的编程笔记。 +我是[Ztiany](http://weibo.com/u/1854760051?refer_flag=1005055010_&is_all=1),我会在这里分享我的学习笔记。 ->搭建博客太麻烦,而且无法一目了然的看到所有的博客,感觉不是很爽-_-! # Java @@ -23,6 +22,30 @@ - [007_Fragment的问题收集](Android/Fragment/007_Fragment问题收集.md) - [008_FragmentDialog使用](Android/Fragment/008_FragmentDialog使用.md) +## View体系 + +### 1 View树的构建 +- [View系统分析01——View树的构建](Android/UI/View体系/View系统分析01——View树的构建.md) +- [View系统分析02——View树遍历的开始](Android/UI/View体系/View系统分析02——View树遍历的开始.md) + +### 2 View的绘制流程与事件分发 +- [001——View基础介绍](Android/UI/View体系/001——View基础介绍.md) +- [002——View绘制流程-measure](Android/UI/View体系/002——View绘制流程-measure.md) +- [003——View绘制流程-onMeasure一般写法与相关总结](Android/UI/View体系/003——View绘制流程-onMeasure一般写法与相关总结.md) +- [004——View绘制流程-layout](Android/UI/View体系/004——View绘制流程-layout) +- [005——View绘制流程-draw](Android/UI/View体系/005——View绘制流程-draw.md) +- [006——View的事件分发源码分析(2.3)](Android/UI/View体系/006——View的事件分发源码分析(2.3).md) +- [007——View事件分发与源码分析](Android/UI/View体系/007——View事件分发与源码分析.md) +- [008——关于View事件分发的总结与滑动冲突](Android/UI/View体系/008——关于View事件分发的总结与滑动冲突.md) +- [009——View实现滑动的方式](Android/UI/View体系/009——View实现滑动的方式.md) +- [010——View滑动冲突常用解决方案](Android/UI/View体系/010——View滑动冲突常用解决方案.md) +- [011——GestureDetector学习](Android/UI/View体系/011——GestureDetector学习.md) +- [012——处理好多指拖动和MotionEvent解析](Android/UI/View体系/012——处理好多指拖动和MotionEvent解析) +- [013——嵌套滑动研究](Android/UI/View体系/013——嵌套滑动研究.md) +- [014——Scroller-OverScroller-VelocityTracker](Android/UI/View体系/014——Scroller-OverScroller-VelocityTracker.md) +- [015——Android系统焦点](Android/UI/View体系/015——Android系统焦点.md) + +### 3 View的绘制技巧 @@ -35,7 +58,7 @@ # 推荐的书 ## Java -- 《EffectiveJava》 +- [《EffectiveJava》](Java/EffectiveJava) - 《Java并发编程的艺术》 - 《java编程思想》

AltStyle によって変換されたページ (->オリジナル) /