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:
+
+
+
+类似ViewPager与多个ListFragemnt嵌套
+
+
+场景2:
+
+
+
+
+类似ViewPager与ViewPager的嵌套
+
+场景3:
+
+
+
+类似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()`
+
+
+具体实现的方式就不贴了,
+
+
+---
+
+## 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方法
+
+上面两种方法基本还是尊重系统的事件分发机制,但是还是有一些情况无法满足,这时候,我们需要根据业务需求来重新定义事件分发了。
+
+比如一个下拉刷新模式
+
+
+
+首先我们定义:
+下拉刷新容器为: 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; + } + } + + + } + + + + +内部拦截法类似,稍微修改一下即可,就不贴代码了 +最后效果如下: + + 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,我们可以监听下面所示的所有手势: + + + +这些手势说明如下: + +| 方法名 | 描述 | +| ------------ | ------------ | +| 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;
+ }
+ }
+
+效果图如下:
+
+
+
+
+但是有没有发现一些不好的地方呢?
+当第一个手指往下拖动了一下控件,接着第二个手指也触摸了屏幕,然后第一个离开了屏幕,这时你会看到红色的子view往上跳动了一下,这个跳动实在是太突兀了,我们希望的应该是当第一个手指离开屏幕时,红色的子view不会有任何跳动,而是依然顺畅的被第二个手指继续拖动。
+
+
+如下面图所示:
+
+
+
+
+
+
+把拖动变得顺畅需要处理多指拖动的情况,而要处理好多指拖动的情况,则需要了解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,效果如下:
+
+
+
+
+
+代码实现很简单:
+
+
+嵌套滑动中的子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自己滑动回到原来的位置。
+
+效果:
+
+
+
+首先我们需要实现拖动的逻辑,然后在手指松开的时候,使用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
+
+比如下面效果:
+
+
+
+可能效果不是很明显,但是我的手指只是快速的划了一个屏幕就松开了,剩下的惯性滑动都是依靠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。
+
+
+
+### 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收到请求后,将会从上至下开始遍历,检查标记,只要有相对应的标记就执行测量/布局/绘制
+
+
+
+
+
+流程图如下所示:
+
+
+
+
+
+---
+
+
+
+
+
+
+
+# 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的一个简单架构图:
+
+
+
+
+
+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在各个版本的不同名称:
+
+
+
+
+
+
+、
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
-```
+
这些重排序可能会导致多线程出现的内存可见性问题。
@@ -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变量与普通变量重排序。
+
+旧的内存模型中程序的操作可能被重排序成下列时序执行:
+
+
+
+
+在旧的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编程思想》