IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android事件分发机制 -> 正文阅读

[移动开发]Android事件分发机制

原文链接:https://juejin.im/post/5eb3e0d6f265da7c002028cd

这次说下Android中的事件分发机制
从开始点击屏幕开始,就会产生从Activity开始到decorview一直到最里层的view一连串事件传递。每一层view或者viewgroup都会首先调用它的dispatchTouchEvent方法,然后判断是否就在当前一层消费掉事件

view的事件分发

首先上一段伪代码,是在书上看到的,也是我觉得总结的最好的

 public?boolean?dispatchTouchEvent(MotionEvent?event)?{
????boolean?isConsume?=?false; 
????if?(isViewGroup)?{ 
????????if?(onInterceptTouchEvent(event))?{
 ???????????isConsume?=?onTouchEvent(event); 
????????}?else?{
 ????????????isConsume?=?child.dispatchTouchEvent(event); 
????????} 

????}?else?{????????
//isView???????
isConsume?=?onTouchEvent(event);???
?    }????
    return?isConsume;
}

复制代码如果当前是viewgroup层级,就会判断 onInterceptTouchEvent 是否为true,如果为true,则代表事件要消费在这一层级,不再往下传递。接着便执行当前 viewgroup 的onTouchEvent方法。如果onInterceptTouchEvent为false,则代表事件继续传递到下一层级的 dispatchTouchEvent方法,接着一样的代码逻辑,一直到最里面一层的view。
ok,还没完哦,到最里面一层就会直接执行onTouchEvent方法,这时候,view有没有权利拒绝消费事件呢? 按道理view作为最底层的,应该是没有发言权才对。但是呢,秉着公平公正原则,view也是可以拒绝的,可以在onTouchEvent方法返回false,表示他不想消费这个事件。那么这个事件又会怎么处理呢?见下面一段伪代码:

public?void?handleTouchEvent(MotionEvent?event)?{
???if?(!onTouchEvent(event))?{
????????getParent.onTouchEvent(event);
????}
}

复制代码如果view的onTouchEvent方法返回false,那么它的父容器的onTouchEvent又会被调用,如果父容器的onTouchEvent又返回false,则又交给上一级。一直到最上层,也就是Activity的onTouchEvent被调用。

至此,消费流程完毕
但是,关于onTouch,onTouchEvent和onClick又是怎么样的调用关系呢?
那就再来一段伪代码:

 public?void?consumeEvent(MotionEvent?event)?{ 
????if?(setOnTouchListener)?{ 
????????onTouch(); 
????????if?(!onTouch())?{ 
????????????onTouchEvent(event);
 ????????} 
????}?else?{
 ????????onTouchEvent(event); 
????}

????if?(setOnClickListener)?{
????????onClick();
????}
}

复制代码当某一层view的onInterceptTouchEvent被调用,则代表当前层级要消费事件。如果它的onTouchListener被设置了的话,则onTouch会被调用,如果onTouch的返回值返回true,则onTouchEvent不会被调用。如果返回false或者没有设置onTouchListener,则会继续调用onTouchEvent。而onClick方法则是设置了onClickListener则会被正常调用。

这里用一张流程图总结下:

在这里插入图片描述

源码分析

一个触摸事件,首先是传到Activity层级,然后传到根view,通过一层层的viewgroup最终到底最里面一层的view,我们来一层层解析
Activity(dispatchTouchEvent)
直接上代码

 //Activity.java 
????public?boolean?dispatchTouchEvent(MotionEvent?ev)?{ ????????if?(ev.getAction()?==?MotionEvent.ACTION_DOWN)?{
 ????????????onUserInteraction(); 
????????} 
????????if?(getWindow().superDispatchTouchEvent(ev))?{
 ????????????return?true;
 ????????} 
????????return?onTouchEvent(ev);
????}

????public?void?onUserInteraction()?{
????}

复制代码这里可以看到,onUserInteraction方法是空的,主要是调用了getWindow().superDispatchTouchEvent(ev)方法,返回true,就代表事件消费了。返回false,就代表下层没人处理,那就直接到了activity的onTouchEvent方法,这点跟之前的消费传递也是吻合的。
继续看看superDispatchTouchEvent方法,然后就走到了PhoneWindow的superDispatchTouchEvent方法,以及DecorView的superDispatchTouchEvent,看看代码:

 ?//PhoneWindow.java 
????private?DecorView?mDecor; 
????@Override
 ????public?boolean?superDispatchTouchEvent(MotionEvent?event)?{ ????????return?mDecor.superDispatchTouchEvent(event);
????} 

????//DecorView.java ????public?boolean?superDispatchTouchEvent(MotionEvent?event)?{
????????return?super.dispatchTouchEvent(event);
????}

复制代码这里可以看到,依次经过了PhoneWindow到达了DecorView,DecorView是activity的根view,也是setcontentView所设置的view的父view,它是继承自FrameLayout。所以这里super.dispatchTouchEvent(event)方法,其实就是走到了viewgroup的dispatchTouchEvent 方法。

ViewGroup(dispatchTouchEvent)

 ???@Override 
????public?boolean?dispatchTouchEvent(MotionEvent?ev)?{ 
????????if?(onFilterTouchEventForSecurity(ev))?{ 
????????????//?Check?for?interception,表示是否拦截的字段 
????????????final?boolean?intercepted; 
????????????if?(actionMasked?==?MotionEvent.ACTION_DOWN 
????????????????????||?mFirstTouchTarget?!=?null)?{ 
??????????????//FLAG_DISALLOW_INTERCEPT标志是通过requestDisallowInterceptTouchEvent设置 ????????????????final?boolean?disallowIntercept?=?(mGroupFlags?&?FLAG_DISALLOW_INTERCEPT)?!=?0;
????????????????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;
????????????}


??????????//mFirstTouchTarget赋值???????????
?while?(target?!=?null)?{
????????????????????final?TouchTarget?next?=?target.next;
????????????????????if?(alreadyDispatchedToNewTouchTarget?&&?target?==?newTouchTarget)?{
????????????????????}?else?{
????????????????????????if?(cancelChild)?{
????????????????????????????if?(predecessor?==?null)?{
????????????????????????????????mFirstTouchTarget?=?next;
????????????????????????????}?else?{
????????????????????????????????predecessor.next?=?next;
????????????????????????????}
????????????????????????????continue;
????????????????????????}
????????????????????}
????????????????}
?????????}

复制代码这里截取了部分关键的代码,首先是两个条件

  • actionMasked == MotionEvent.ACTION_DOWN

  • mFirstTouchTarget != null

如果满足了其中一个条件才会继续走下去,执行onInterceptTouchEvent方法等,否则就直接intercepted = true,表示拦截。
第一个条件很明显,就是表示当前事件位按下事件(ACTION_DOWN)
第二个条件是个字段,根据下面的代码可以得知,当后面有view消费掉事件的时候,这个mFirstTouchTarget字段就会赋值,否则就为空。

所以什么意思呢,当ACTION_DOWN事件时候,一定会执行到后面代码。当其他事件来的时候,要看当前viewgroup是否消费了事件,如果当前viewgroup已经消费了事件,没传到子view,那么mFirstTouchTarget字段就为空,所以就不会执行到后面的代码,就直接消费掉所有事件了。

这就符合了之前的所说的一种机制:
某个view一旦开始拦截,那么后续事件就全部就给它处理了,也不会执行onInterceptTouchEvent方法了

但是,两个条件满足了一个,就能执行到onInterceptTouchEvent了吗?不一定,这里看到还有一个判断条件:disallowIntercept。这个字段是由requestDisallowInterceptTouchEvent方法设置的,后面我们会讲到,主要用于滑动冲突,意思就是子view告诉你不想让你拦截,那么你就不拦截了,直接返回false。

ok,继续看源码,之前的内容我们了解到,如果viewgroup不拦截事件,应该会传递给子view,那在哪里传的呢?继续看看dispatchTouchEvent的代码:

 if?(!canceled?&&?!intercepted)?{ 
????????????????????final?int?childrenCount?=?mChildrenCount; ????????????????????if?(newTouchTarget?==?null?&&?childrenCount?!=?0)?{ ????????????????????????final?View[]?children?=?mChildren; 
???????????????????????for?(int?i?=?childrenCount?-?1;?i?>=?0;?i--)?{ ????????????????????????????final?int?childIndex?=?getAndVerifyPreorderedIndex( ????????????????????????????????????childrenCount,?i,?customOrder); ????????????????????????????final?View?child?=?getAndVerifyPreorderedView( ????????????????????????????????????preorderedList,?children,?childIndex);

????????????????????????????if?(childWithAccessibilityFocus?!=?null)?{
????????????????????????????????if?(childWithAccessibilityFocus?!=?child)?{
????????????????????????????????????continue;
????????????????????????????????}
????????????????????????????????childWithAccessibilityFocus?=?null;
????????????????????????????????i?=?childrenCount?-?1;
????????????????????????????}

????????????????????????????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);
????????????????????????????if?(dispatchTransformedTouchEvent(ev,?false,?child,?idBitsToAssign))?{
???????????????????????????????//?Child?wants?to?receive?touch?within?its?bounds.
????????????????????????????????mLastTouchDownTime?=?ev.getDownTime();
????????????????????????????????if?(preorderedList?!=?null)?{
????????????????????????????????????//?childIndex?points?into?presorted?list,?find?original?index
????????????????????????????????????for?(int?j?=?0;?j?<?childrenCount;?j++)?{
????????????????????????????????????????if?(children[childIndex]?==?mChildren[j])?{
????????????????????????????????????????????mLastTouchDownIndex?=?j;
????????????????????????????????????????????break;
????????????????????????????????????????}
????????????????????????????????????}
????????????????????????????????}?else?{
????????????????????????????????????mLastTouchDownIndex?=?childIndex;
????????????????????????????????}
????????????????????????????????mLastTouchDownX?=?ev.getX();
????????????????????????????????mLastTouchDownY?=?ev.getY();
????????????????????????????????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();
????????????????????}

????????????????}
????????????}

复制代码这里可以看到,进行了一个子view的便利,其中,如果满足两个条件中的一个,就跳出。否则就执行dispatchTransformedTouchEvent方法。先看看这两个条件:

  • !child.canReceivePointerEvents()

  • !isTransformedTouchPointInView(x, y, child, null)

看名字是看不出啥了,直接看代码吧:

 protected?boolean?canReceivePointerEvents()?{ ????????return?(mViewFlags?&?VISIBILITY_MASK)?==?VISIBLE?||?getAnimation()?!=?null; 
????} 

????protected?boolean?isTransformedTouchPointInView(float?x,?float?y,?View?child, ????????????PointF?outLocalPoint)?{ 
????????final?float[]?point?=?getTempPoint(); 
????????point[0]?=?x;
 ????????point[1]?=?y;
????????transformPointToViewLocal(point,?child);
????????final?boolean?isInView?=?child.pointInView(point[0],?point[1]);
????????if?(isInView?&&?outLocalPoint?!=?null)?{
????????????outLocalPoint.set(point[0],?point[1]);
????????}
????????return?isInView;
????}

复制代码哦,原来是这个意思。canReceivePointerEvents方法就代表view是不是可以接受点击事件,比如是不是在播放动画。而isTransformedTouchPointInView方法代表点击事件的坐标是不是在这个view的区域上面。
ok,如果条件都满足,就执行到dispatchTransformedTouchEvent方法了:

 private?boolean?dispatchTransformedTouchEvent(MotionEvent?event,?boolean?cancel, ????????????View?child,?int?desiredPointerIdBits)?{ 
????????final?boolean?handled; 

????????//?Canceling?motions?is?a?special?case.??We?don't?need?to?perform?any?transformations ????????//?or?filtering.??The?important?part?is?the?action,?not?the?contents. ????????final?int?oldAction?=?event.getAction();
 ????????if?(cancel?||?oldAction?==?MotionEvent.ACTION_CANCEL)?{ 
???????????event.setAction(MotionEvent.ACTION_CANCEL);
????????????if?(child?==?null)?{
????????????????handled?=?super.dispatchTouchEvent(event);
????????????}?else?{
????????????????handled?=?child.dispatchTouchEvent(event);
????????????}
????????????event.setAction(oldAction);
????????????return?handled;
????????}
}

复制代码这个方法大家应该都猜到了,其实就是执行了child.dispatchTouchEvent(event)。也就是下一层view的dispatchTouchEvent方法呗,开始事件的层级传递。

View(dispatchTouchEvent)

到view 层级的时候,自然就执行的view的dispatchTouchEvent,上代码

 ??public?boolean?dispatchTouchEvent(MotionEvent?event)?{ 
????????boolean?result?=?false; 
?
???????if?(mInputEventConsistencyVerifier?!=?null)?{ ????????????mInputEventConsistencyVerifier.onTouchEvent(event,?0); 
????????} 

????????final?int?actionMasked?=?event.getActionMasked(); ????????if?(actionMasked?==?MotionEvent.ACTION_DOWN)?{
????????????//?Defensive?cleanup?for?new?gesture
????????????stopNestedScroll();
????????}

????????if?(onFilterTouchEventForSecurity(event))?{
????????????if?((mViewFlags?&?ENABLED_MASK)?==?ENABLED?&&?handleScrollBarDragging(event))?{
????????????????result?=?true;
????????????}
????????????//noinspection?SimplifiableIfStatement
????????????ListenerInfo?li?=?mListenerInfo;
????????????if?(li?!=?null?&&?li.mOnTouchListener?!=?null
????????????????????&&?(mViewFlags?&?ENABLED_MASK)?==?ENABLED
????????????????????&&?li.mOnTouchListener.onTouch(this,?event))?{
????????????????result?=?true;
????????????}

????????????if?(!result?&&?onTouchEvent(event))?{
????????????????result?=?true;
????????????}
????????}

????????if?(!result?&&?mInputEventConsistencyVerifier?!=?null)?{
????????????mInputEventConsistencyVerifier.onUnhandledEvent(event,?0);
????????}

????????//?Clean?up?after?nested?scrolls?if?this?is?the?end?of?a?gesture;
????????//?also?cancel?it?if?we?tried?an?ACTION_DOWN?but?we?didn't?want?the?rest
????????//?of?the?gesture.
????????if?(actionMasked?==?MotionEvent.ACTION_UP?||
????????????????actionMasked?==?MotionEvent.ACTION_CANCEL?||
????????????????(actionMasked?==?MotionEvent.ACTION_DOWN?&&?!result))?{
????????????stopNestedScroll();
????????}

????????return?result;
????}

复制代码这里可以看到,首先会判断li.mOnTouchListener != null,如果不为空,就会执行onTouch方法。
根据onTouch方法返回的结果,如果为false,result就为false,那么onTouchEvent才会执行。这个逻辑也是符合我们之前说的传递方式。
最后我们再看看view的onTouchEvent都做了什么事:

 final?boolean?clickable?=?((viewFlags?&?CLICKABLE)?==?CLICKABLE ????????????????||?(viewFlags?&?LONG_CLICKABLE)?==?LONG_CLICKABLE) ????????????????||?(viewFlags?&?CONTEXT_CLICKABLE)?==?CONTEXT_CLICKABLE; 

 if?(clickable?||?(viewFlags?&?TOOLTIP)?==?TOOLTIP)?{ 
????????????switch?(action)?{ 
????????????????case?MotionEvent.ACTION_UP: ????????????????????boolean?prepressed?=?(mPrivateFlags?&?PFLAG_PREPRESSED)?!=?0; ????????????????????if?((mPrivateFlags?&?PFLAG_PRESSED)?!=?0?||?prepressed)?{
????????????????????????//?take?focus?if?we?don't?have?it?already?and?we?should?in
????????????????????????//?touch?mode.
????????????????????????boolean?focusTaken?=?false;
????????????????????????if?(isFocusable()?&&?isFocusableInTouchMode()?&&?!isFocused())?{
????????????????????????????focusTaken?=?requestFocus();
????????????????????????}

????????????????????????if?(!mHasPerformedLongPress?&&?!mIgnoreNextUpEvent)?{
????????????????????????????//?This?is?a?tap,?so?remove?the?longpress?check
????????????????????????????removeLongPressCallback();

????????????????????????????//?Only?perform?take?click?actions?if?we?were?in?the?pressed?state
????????????????????????????if?(!focusTaken)?{
????????????????????????????????//?Use?a?Runnable?and?post?this?rather?than?calling
????????????????????????????????//?performClick?directly.?This?lets?other?visual?state
????????????????????????????????//?of?the?view?update?before?click?actions?start.
????????????????????????????????if?(mPerformClick?==?null)?{
????????????????????????????????????mPerformClick?=?new?PerformClick();
????????????????????????????????}
????????????????????????????????if?(!post(mPerformClick))?{
????????????????????????????????????performClickInternal();
????????????????????????????????}
????????????????????????????}
????????????????????????}
????????????????????}
????????????????????mIgnoreNextUpEvent?=?false;
????????????????????break;
????????????}

????????????return?true;
????????}

复制代码从代码可以得知,如果设置了CLICKABLE或者LONG_CLICKABLE,那么这个view就会消费事件,并且执行performClickInternal方法,然后执行到performClick方法。这个performClick方法大家应该都很熟悉,就是触发点击的方法,其实内部就是执行了onClick方法。

 ????private?boolean?performClickInternal()?{ 
????????notifyAutofillManagerOnClick();
????????return?performClick(); 
????} 

????public?boolean?performClick()?{ 
????????final?boolean?result; 
????????final?ListenerInfo?li?=?mListenerInfo; ????????if?(li?!=?null?&&?li.mOnClickListener?!=?null)?{
????????????playSoundEffect(SoundEffectConstants.CLICK);
????????????li.mOnClickListener.onClick(this);
????????????result?=?true;
????????}?else?{
????????????result?=?false;
????????}
????????return?result;
????}

复制代码至此,源代码也看的差不多了,内部其实有很多细节,这里也就不一一说明了,大家有空可以去研究下。

事件分发的应用(requestDisallowInterceptTouchEvent)

那既然学会了事件分发机制,我们实际工作中会怎么应用呢?其实最常见的就是解决滑动冲突的问题。一般有两种解决办法:

  • 一种是外部拦截:从父view端处理,根据情况决定事件是否分发到子view

  • 一种是内部拦截:从子view端处理,根据情况决定是否阻止父view进行拦截,其中的关键就是requestDisallowInterceptTouchEvent方法。

第一种方法,其实就是在onInterceptTouchEvnet方法里面进行判断返回true还是返回false。
第二种方法,就是用到了requestDisallowInterceptTouchEvent方法,这个方法的意思就是让父view不要去拦截事件了,在dispatchTouchEvent方法里面就有这个标志位:FLAG_DISALLOW_INTERCEPT,如果disallowIntercept字段为true,就不会去执行onInterceptTouchEvent方法,而是返回false,不拦截事件。
上代码:

 ????//外部拦截法:父view.java???????? 
????@Override 
????public?boolean?onInterceptTouchEvent(MotionEvent?ev)?{ 
????????boolean?intercepted?=?false; 
???????//父view拦截条件 
????????boolean?parentCanIntercept; 

????????switch?(ev.getActionMasked())?{ ????????????
    case?MotionEvent.ACTION_DOWN:
????????????????intercepted?=?false;
????????????????break;
????????????case?MotionEvent.ACTION_MOVE:
????????????????if?(parentCanIntercept)?{
????????????????????intercepted?=?true;
????????????????}?else?{
????????????????????intercepted?=?false;
????????????????}
????????????????break;
????????????case?MotionEvent.ACTION_UP:
????????????????intercepted?=?false;
????????????????break;
????????}
????????return?intercepted;

???}

复制代码外部拦截很简单,就是判断条件,然后决定是否进行拦截。

 ????//父view.java???????????? 
????@Override 
????public?boolean?onInterceptTouchEvent(MotionEvent?ev)?{ ????????if?(ev.getActionMasked()?==?MotionEvent.ACTION_DOWN)?{ 
????????????return?false; 
????????}?else?{ 
????????????return?true; 
????????} 
????}

????//子view.java
????@Override
????public?boolean?dispatchTouchEvent(MotionEvent?event)?{
????????//父view拦截条件????????
boolean?parentCanIntercept;
????????switch?(event.getActionMasked())?{
????????????case?MotionEvent.ACTION_DOWN:
????????????????getParent().requestDisallowInterceptTouchEvent(true);
????????????????break;
????????????case?MotionEvent.ACTION_MOVE:
????????????????if?(parentCanIntercept)?{
????????????????????getParent().requestDisallowInterceptTouchEvent(false);
????????????????}
????????????????break;
????????????case?MotionEvent.ACTION_UP:
????????????????break;
????????}
????????return?super.dispatchTouchEvent(event);
????}

复制代码感觉内部拦截有点复杂呀,还要重写父view的方法,这里分析下,为什么要去这么写:

  • 父view ACTION_DOWN的时候,不能拦截,因为如果拦截,那么后续事件也就跟子view无关了

  • 父view 其他事件的时候,要返回true,表示拦截。因为onInterceptTouchEvent方法的调用是被FLAG_DISALLOW_INTERCEPT标志位所控制,所以子view需要父view拦截的时候,才会走到这个onInterceptTouchEvent方法中来,那么这时候要保证方法中一定是要拦截的。

至此,事件的分发机制也就说的差不多了。

文末

您的点赞收藏就是对我最大的鼓励!
欢迎关注我,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,欢迎在评论区一起留言讨论!

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-12-08 13:55:57  更:2021-12-08 13:58:08 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 7:40:41-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码