当前位置: 移动技术网 > IT编程>移动开发>Android > Android View事件机制 21问21答

Android View事件机制 21问21答

2019年07月24日  | 移动技术网IT编程  | 我要评论

1.view的坐标参数 主要有哪些?分别有什么注意的要点?

答:left,right,top,bottom 注意这4个值其实就是 view 和 他的父控件的 相对坐标值。 并非是距离屏幕左上角的绝对值,这点要注意。

  此外,x和y 其实也是相对于父控件的坐标值。 translationx,translationy 这2个值 默认都为0,是相对于父控件的左上角的偏移量。

  换算关系:

  x=left+tranx,y=top+trany.

  很多人不理解,为什么事这样,其实就是view 如果有移动的话,比如平移这种,你们就要注意了,top和left 这种值 是不会变化的。

无论你把view怎么拖动,但是 x,y,tranx,trany 的值是随着拖动平移 而变化的。想明白这点 就行了。

2.ontouchevent和gesturedetector 在什么时候用哪个比较好?

答:只有滑动需求的时候 就用前者,如果有双击等这种行为的时候 就用后者。

3.scroller 用来解决什么问题?

答:view的scrollto和scrollby 滑动效果太差了,是瞬间完成。而scroller可以配合view的computescroll 来完成 渐变的滑动效果。体验更好。

4.scrollto和scrollby 有什么需要注意的?

答:前者是绝对滑动,后者是相对滑动。滑动的是view的内容 而不是view本身。这很重要。比如textview 调用这2个方法 滑动的就是显示出来的字的内容。

一般而言 我们用scrollby会比较多一些。传值的话 其实 记住几个法则就可以了。 右-左 x为正 否则x为负 上-下 y为负,否则y为正。

可以稍微看一下 这2个的源码:

public void scrollto(int x, int y) {
if (mscrollx != x || mscrolly != y) {
int oldx = mscrollx;
int oldy = mscrolly;
mscrollx = x;
mscrolly = y;
invalidateparentcaches();
onscrollchanged(mscrollx, mscrolly, oldx, oldy);
if (!awakenscrollbars()) {
postinvalidateonanimation();
}
}
}
public void scrollby(int x, int y) {
scrollto(mscrollx + x, mscrolly + y);
}

看到里面有2个变量 mscrollx 和mscrolly 这2个东西没,这2个单位的 值是像素,前者代表 view的左边缘和view内容左边缘的距离。 后者代表 view上边缘和view内容上边缘的距离。

5.使用动画来实现view的滑动 有什么后果?

答:实际上view动画 是对view的表面ui 也就是给用户呈现出的视觉效果 来做的移动,动画本身并不能移动view的真正位置。属性动画除外。动画播放结束以后,view最终还是会回到自己的位置的,。当然了你可以设置fillafter 属性 来让动画播放结束以后 view表象停留在 变化以后的位置。所以这会带来一个很严重的后果。比如你的button在屏幕的左边,你现在用个动画 并且设置了fillafter属性让他去了右边。你会发现 点击右边的button 没有click事件触发,但是点击左边的 却可以触发,原因就是右边的button 只是view的表象,真正的button 还在左边没有动过。你一定要这么做的话 可以提前在右边button移动后的位置放一个新的button,当你动画执行结束以后 把右边的enable 左边的让他gone就可以了。

这么做就可以规避上述问题。

6.让view滑动总共有几种方式,分别要注意什么?都适用于那些场景?

答:总共有三种:

a:scrollto,scrollby。这种是最简单的,但是只能滑动view的内容 不可以滑动view本身。

b:动画。动画可以滑动view内容,但是注意非属性动画 就如我们问题5说的内容 会影响到交互,使用的时候要多注意。不过多数复杂的滑动效果都是属性动画来完成的,属于大杀器级别、

c:改变布局参数。这种最好理解了,无非是动态的通过java代码来修改 margin等view的参数罢了。不过用的比较少。我本人不怎么用这种方法。

7.scroller是干嘛的?原理是什么?

答:scroller就是用于 让view有滑动渐变效果的。用法如下:

package com.example.administrator.motioneventtest;
import android.content.context;
import android.util.attributeset;
import android.widget.scroller;
import android.widget.textview;
/**
* created by administrator on //.
*/
public class customtextview extends textview{
private scroller mscroller;

public customtextview(context context) {
super(context);
mscroller=new scroller(context);
}
public customtextview(context context, attributeset attrs) {
super(context, attrs);
mscroller=new scroller(context);
}
public customtextview(context context, attributeset attrs, int defstyleattr) {
super(context, attrs, defstyleattr);
mscroller=new scroller(context);
}
//调用此方法滚动到目标位置
public void smoothscrollto(int fx, int fy) {
int dx = fx - mscroller.getfinalx();
int dy = fy - mscroller.getfinaly();
smoothscrollby(dx, dy);
}
//调用此方法设置滚动的相对偏移
public void smoothscrollby(int dx, int dy) {
//设置mscroller的滚动偏移量
mscroller.startscroll(mscroller.getfinalx(), mscroller.getfinaly(), dx, dy,);
invalidate();//这里必须调用invalidate()才能保证computescroll()会被调用,否则不一定会刷新界面,看不到滚动效果
}
//使用scroller最重要不要遗漏这个方法
@override
public void computescroll() {
if (mscroller.computescrolloffset())
{
scrollto(mscroller.getcurrx(),mscroller.getcurry());
//这个方法不要忘记调用。
postinvalidate();
}
super.computescroll();
}
}

其实上述代码 很多人应该都能搜到。我们这里主要讲一下 他的原理。

//参数很好理解 前面滑动起始点 中间滑动距离 最后一个是 渐变时间
//而且我们看到startscroll 这个方法就是设置了一下参数 并没有什么滑动的代码在
//回到前面的demo能看到我们通常调用完这个方法以后 都会马上调用invalidate()方法
public void startscroll(int startx, int starty, int dx, int dy, int duration) {
mmode = scroll_mode;
mfinished = false;
mduration = duration;
mstarttime = animationutils.currentanimationtimemillis();
mstartx = startx;
mstarty = starty;
mfinalx = startx + dx;
mfinaly = starty + dy;
mdeltax = dx;
mdeltay = dy;
mdurationreciprocal = .f / (float) mduration;
}
//我们都知道invalidate 会触发view的 draw方法 
//我们跟进去看 会发现draw方法里 会调用下面的代码:
//也就是说会调用 computescroll方法 而view本身这个方法
//是空的所以会留给我们自己实现
int sx = ;
int sy = ;
if (!drawingwithrendernode) {
computescroll();
sx = mscrollx;
sy = mscrolly;
}
//然后回到我们的customtextview 可以看到我们实现的 computescroll方法如下:
//你看在这个方法里 我们调用了scrollto方法 来实现滑动,滑动结束以后再次触发view的重绘
//然后又会再次触发computescroll 实现一个循环。
public void computescroll() {
if (mscroller.computescrolloffset())
{
scrollto(mscroller.getcurrx(),mscroller.getcurry());
//这个方法不要忘记调用。
postinvalidate();
}
super.computescroll();
} 
//返回true就代表滑动还没结束 false就是结束了
//其实这个方法 就跟属性动画里的插值器一样 你在使用startscroll方法的时候 会传一个事件的值,
//这个方法就是根据这个事件的值来计算你每一次scrollx和scrolly的值
public boolean computescrolloffset() {
if (mfinished) {
return false;
}
int timepassed = (int)(animationutils.currentanimationtimemillis() - mstarttime);
if (timepassed < mduration) {
switch (mmode) {
case scroll_mode:
final float x = minterpolator.getinterpolation(timepassed * mdurationreciprocal);
mcurrx = mstartx + math.round(x * mdeltax);
mcurry = mstarty + math.round(x * mdeltay);
break;
case fling_mode:
final float t = (float) timepassed / mduration;
final int index = (int) (nb_samples * t);
float distancecoef = .f;
float velocitycoef = .f;
if (index < nb_samples) {
final float t_inf = (float) index / nb_samples;
final float t_sup = (float) (index + ) / nb_samples;
final float d_inf = spline_position[index];
final float d_sup = spline_position[index + ];
velocitycoef = (d_sup - d_inf) / (t_sup - t_inf);
distancecoef = d_inf + (t - t_inf) * velocitycoef;
}
mcurrvelocity = velocitycoef * mdistance / mduration * .f;
mcurrx = mstartx + math.round(distancecoef * (mfinalx - mstartx));
// pin to mminx <= mcurrx <= mmaxx
mcurrx = math.min(mcurrx, mmaxx);
mcurrx = math.max(mcurrx, mminx);
mcurry = mstarty + math.round(distancecoef * (mfinaly - mstarty));
// pin to mminy <= mcurry <= mmaxy
mcurry = math.min(mcurry, mmaxy);
mcurry = math.max(mcurry, mminy);
if (mcurrx == mfinalx && mcurry == mfinaly) {
mfinished = true;
}
break;
}
}
else {
mcurrx = mfinalx;
mcurry = mfinaly;
mfinished = true;
}
return true;
}

8.view的滑动渐变效果总共有几种方法?

答:三种,第一种是scroller 也是使用最多的。问题7里有解释。还有一种就是动画,动画我就不多说了,不属于本文范畴。最后一种也是我们经常使用的就是用handler ,每隔一个时间间隔 来更新view的状态。

代码不写了很简单。 自行体会。

9.view的事件传递机制 如何用伪代码来表示?

答:

/**
* 对于一个root viewgroup来说,如果接受了一个点击事件,那么首先会调用他的dispatchtouchevent方法。
* 如果这个viewgroup的onintercepttouchevent 返回true,那就代表要拦截这个事件。接下来这个事件就
* 给viewgroup自己处理了,从而viewgroup的ontouchevent方法就会被调用。如果如果这个viewgroup的onintercepttouchevent
* 返回false就代表我不拦截这个事件,然后就把这个事件传递给自己的子元素,然后子元素的dispatchtouchevent
* 就会被调用,就是这样一个循环直到 事件被处理。
*
*/
public boolean dispatchtouchevent(motionevent ev)
{
boolean consume=false;
if (onintercepttouchevent(ev)) {
consume=ontouchevent(ev);
}else
{
consume=child.dispatchtouchevent(ev);
}
return consume;
} 

10.view的ontouchevent,onclicklisterner和ontouchlistener的ontouch方法 三者优先级如何?

答:ontouchlistener优先级最高,如果ontouch方法返回 false ,那ontouchevent就被调用了,返回true 就不会被调用。至于onclick 优先级最低。

11.点击事件的传递顺序如何?

答:activity-window-view。从上到下依次传递,当然了如果你最低的那个view ontouchevent返回false 那就说明他不想处理 那就再往上抛,都不处理的话

最终就还是让activity自己处理了。举个例子,pm下发一个任务给leader,leader自己不做 给架构师a,小a也不做 给程序员b,b如果做了那就结束了这个任务。

b如果发现自己搞不定,那就找a做,a要是也搞不定 就会不断向上发起请求,最终可能还是pm做。

//activity的dispatchtouchevent 方法 一开始就是交给window去处理的
//win的superdispatchtouchevent 返回true 那就直接结束了 这个函数了。返回false就意味
//这事件没人处理,最终还是给activity的ontouchevent 自己处理 这里的getwindow 其实就是phonewindow
public boolean dispatchtouchevent(motionevent ev) {
if (ev.getaction() == motionevent.action_down) {
onuserinteraction();
}
if (getwindow().superdispatchtouchevent(ev)) {
return true;
}
return ontouchevent(ev);
}
//来看phonewindow的这个函数 直接把事件传递给了mdecor
@override
public boolean superdispatchtouchevent(motionevent event) {
return mdecor.superdispatchtouchevent(event);
}
//devorview就是 我们的rootview了 就是那个framelayout 我们的setcontentview里面传递的那个layout
//就是这个decorview的 子view了
@override
public final view getdecorview() {
if (mdecor == null) {
installdecor();
}
return mdecor;
}

12.事件分为几个步骤?

答:down事件开头,up事件结尾,中间可能会有数目不定的move事件。

13.viewgroup如何对点击事件分发?

答:

viewgroup就是在actionmasked == motionevent.action_down 和 mfirsttouchtarget != null 这两种情况来判断是否会进入拦截事件的流程看代码可以知道 如果是action_down事件 那就肯定进入 是否要拦截事件的流程如果不是action_down事件 那就要看mfirsttouchtarget != null 这个条件是否成立这个地方有点绕但是也好理解,其实就是 对于一个事件序列来说 down是事件的开头 所以肯定进入了这个事件是否拦截的流程 也就是if 括号内。
mfirsttouchtarget其实是一个单链表结构他指向的是 成功处理事件的子元素。
也就是说 如果有子元素成功处理了 事件,那这个值就不为null。反过来说只要viewgroup拦截了事件,mfirsttouchtarget就不为null,所以括号内就不会执行,也就侧面说明了一个结论:

某个view 一旦决定拦截事件,那么这个事件所属的事件序列 都只能由他来执行。并且onintercepttouchevent 这个方法不会被调用了

final boolean intercepted;
if (actionmasked == motionevent.action_down
|| mfirsttouchtarget != null) {
final boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != ;
if (!disallowintercept) {
intercepted = onintercepttouchevent(ev);
ev.setaction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// there are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
} 

14.:果某个view 处理事件的时候 没有消耗down事件 会有什么结果?

答:假如一个view,在down事件来的时候 他的ontouchevent返回false, 那么这个down事件 所属的事件序列 就是他后续的move 和up 都不会给他处理了,全部都给他的父view处理。

15.如果view 不消耗move或者up事件 会有什么结果?

答:那这个事件所属的事件序列就消失了,父view也不会处理的,最终都给activity 去处理了。

16.viewgroup 默认拦截事件吗?

答:默认不拦截任何事件,onintercepttouchevent返回的是false。

17.一旦有事件传递给view,view的ontouchevent一定会被调用吗?

答:是的,因为view 本身没有onintercepttouchevent方法,所以只要事件来到view这里 就一定会走ontouchevent方法。

并且默认都是消耗掉,返回true的。除非这个view是不可点击的,所谓不可点击就是clickable和longgclikable同时为fale

button的clickable就是true 但是textview是false。

18.enable是否影响view的ontouchevent返回值?

答:不影响,只要clickable和longclickable有一个为真,那么ontouchevent就返回true。

19.requestdisallowintercepttouchevent 可以在子元素中干扰父元素的事件分发吗?如果可以,是全部都可以干扰吗?

答:肯定可以,但是down事件干扰不了。

20.dispatchtouchevent每次都会被调用吗?

答:是的,onintercepttouchevent则不会。

21.滑动冲突问题如何解决 思路是什么?

答。要解决滑动冲突 其实最主要的就是有一个核心思想。你到底想在一个事件序列中,让哪个view 来响应你的滑动?比如 从上到下滑,是哪个view来处理这个事件,从左到右呢?

用业务需求 来想明白以后 剩下的 其实就很好做了。核心的方法 就是2个 外部拦截也就是父亲拦截,另外就是内部拦截,也就是子view拦截法。 学会这2种 基本上所有的滑动冲突都是这2种的变种,而且核心代码思想都一样。

外部拦截法:思路就是重写父容器的onintercepttouchevent即可。子元素一般不需要管。可以很容易理解,因为这和android自身的事件处理机制 逻辑是一模一样的

@override
public boolean onintercepttouchevent(motionevent ev) {
boolean intercepted = false;
int x = (int) ev.getx();
int y = (int) ev.gety();
switch (ev.getaction()) {
//down事件肯定不能拦截 拦截了后面的就收不到了
case motionevent.action_down:
intercepted = false;
break;
case motionevent.action_move:
if (你的业务需求) {
//如果确定拦截了 就去自己的ontouchevent里 处理拦截之后的操作和效果 即可了
intercepted = true;
} else {
intercepted = false;
}
break;
case motionevent.action_up:
//up事件 我们一般都是返回false的 一般父容器都不会拦截他。 因为up是事件的最后一步。这里返回true也没啥意义
//唯一的意义就是因为 父元素 up被拦截。导致子元素 收不到up事件,那子元素 就肯定没有onclick事件触发了,这里的
//小细节 要想明白
intercepted = false;
break;
default:
break;
}
return intercepted;
} 

内部拦截法:内部拦截法稍微复杂一点,就是事件到来的时候,父容器不管,让子元素自己来决定是否处理。如果消耗了 就最好,没消耗 自然就转给父容器处理了。

子元素代码:

@override
public boolean dispatchtouchevent(motionevent event) {
int x = (int) event.getx();
int y = (int) event.gety();
switch (event.getaction()) {
case motionevent.action_down:
getparent().requestdisallowintercepttouchevent(true);
break;
case motionevent.action_move:
if (如果父容器需要这个点击事件) {
getparent().requestdisallowintercepttouchevent(false);
}//否则的话 就交给自己本身view的ontouchevent自动处理了
break;
case motionevent.action_up:
break;
default:
break;
}
return super.dispatchtouchevent(event);
} 

父亲容器代码也要修改一下,其实就是保证父亲别拦截down:

@override
public boolean onintercepttouchevent(motionevent ev) {
if (ev.getaction() == motionevent.action_down) {
return false;
}
return true;
}

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网