摘要:在收到事件时,找到离自己最近的,与它进行绑定并关闭它的事件拦截机制。所以需要主动拦截这种事件,拦截标准超过的距离。当前触摸的不是支持机制的。这种机制将滑动事件的传递封装起来,通过辅助类实现和之间的连接和交互。
一、从一个简单的DEMO看什么是嵌套滚动
我们先来看一下DEMO的效果,直观的感受一下什么是嵌套滚动:
在解释上图涉及到哪些嵌套滑动操作之前,我先贴一下嵌套布局的xml结构:
其中:
NestedWebViewRecyclerViewGroup为最外层滑动容器;
com.wzy.nesteddetail.view.NestedScrollWebView为布局顶部可嵌套滑动的View;
TextView为布局中部不可滑动的View;
android.support.v7.widget.RecyclerView为布局底部可滑动的View;
现在我们来说明一下,简单的DEMO效果中包含了哪些嵌套滑动操作:
向上滑动顶部WebView时,首先滑动WebView的内容,WebView的内容滑动到底后再滑动外层容器。外层容器滑动到RecyclerView完全露出后,再将滑动距离或者剩余速度传递给RecyclerView继续滑动.
滑动底部RecyclerView时,首先滑动RecyclerView的内容,RecyclerView的内容滑动到顶后再滑动外层容器。外层容器也滑动到顶后,再将滑动距离或者剩余速度传递给WebView继续滑动.
触摸本身不可滑动的TextView时,滑动事件被外层容器拦截。外层容器根据滑动方向和是否滑动到相应阈值,再将相应的滑动距离或者速度传递给WebView或者RecyclerView.
再不知道NestedScrolling机制之前,我相信大部分人想实现上面的滑动效果都是比较头大的,特别是滑动距离和速度要从WebView->外层容器->RecyclerView并且还要支持反向传递。
有了Google提供的牛逼嵌套滑动机制,再加上这篇文章粗浅的科普,我相信大部分人都能够实现这种滑动效果。这种效果最常见的应用场景就是各种新闻客户端的详情页。
Android在support.v4包中提供了用于View支持嵌套滑动的两个接口:
NestedScrollingParent
NestedScrollingChild
我先用比较白话的语言介绍一下NestedScrolling的工作原理:
Google从逻辑上区分了滑动的两个角色:NestedScrollingParent简称ns parent,NestedScrollingChild简称ns child。对应了滑动布局中的外层滑动容器和内部滑动容器。
ns child在收到DOWN事件时,找到离自己最近的ns parent,与它进行绑定并关闭它的事件拦截机制。
ns child会在接下来的MOVE事件中判定出用户触发了滑动手势,并把事件拦截下来给自己消费。
消费MOVE事件流时,对于每一个MOVE事件增加的滑动距离:
4.1. ns child并不是直接自己消费,而是先将它交给ns parent,让ns parent可以在ns child滑动前进行消费。
4.2. 如果ns parent没有消费或者滑动没消费完,ns child再消费剩下的滑动。
4.3. 如果ns child消费后滑动还是有剩余,会把剩下的滑动距离再交给ns parent消费。
4.4. 最后如果ns parent消费滑动后还有剩余,ns child可以做最终处理。
ns child在收到UP事件时,可以计算出需要滚动的速度,ns child对于速度的消费流程是:
5.1 ns child在进行flying操作前,先询问ns parent是否需要消费该速度。如果ns parent消费该速度,后续就由ns parent带飞,自己就不消费该速度了。如果ns parent不消费,则ns child进行自己的flying操作。
5.2 ns child在flying过程中,如果已经滚动到阈值速度仍没有消费完,会再次将速度分发给ns parent,将ns parent进行消费。
NestedScrollingParent和NestedScrollingChild的源码定义也是为了配合滑动实现定义出来的:
NestedScrollingChild
void setNestedScrollingEnabled(boolean enabled); // 设置是否开启嵌套滑动 boolean isNestedScrollingEnabled(); // 获得设置开启了嵌套滑动 boolean startNestedScroll(@ScrollAxis int axes); // 沿给定的轴线开始嵌套滚动 void stopNestedScroll(); // 停止当前嵌套滚动 boolean hasNestedScrollingParent(); // 如果有ns parent,返回true boolean dispatchNestedPreScroll(int dx , int dy , @Nullable int[] consumed , @Nullable int[] offsetInWindow); // 消费滑动时间前,先让ns parent消费 boolean dispatchNestedScroll(int dxConsumed , int dyConsumed , int dxUnconsumed , int dyUnconsumed , @Nullable int[] offsetInWindow); // ns parent消费ns child剩余滚动后是否还有剩余。return true代表还有剩余 boolean dispatchNestedPreFling(float velocityX , float velocityY); // 消费fly速度前,先让ns parent消费 boolean dispatchNestedFling(float velocityX , float velocityY , boolean consumed); // ns parent消费ns child消费后的速度之后是否还有剩余。return true代表还有剩余
NestedScrollingParent
boolean onStartNestedScroll(@NonNull View var1 , @NonNull View var2 , int var3); // 决定是否接收子View的滚动事件 void onNestedScrollAccepted(@NonNull View var1 , @NonNull View var2 , int var3); // 响应子View的滚动 void onStopNestedScroll(@NonNull View var1); // 滚动结束的回调 void onNestedPreScroll(@NonNull View var1 , int var2 , int var3 , @NonNull int[] var4); // ns child滚动前回调 void onNestedScroll(@NonNull View var1 , int var2 , int var3 , int var4 , int var5); // ns child滚动后回调 boolean onNestedPreFling(@NonNull View var1 , float var2 , float var3); // ns child flying前回调 boolean onNestedFling(@NonNull View var1 , float var2 , float var3 , boolean var4); // ns child flying后回调 int getNestedScrollAxes(); // 返回当前布局嵌套滚动的坐标轴
Google为了让开发者更加方便的实现这两个接口,提供了NestedScrollingParentHelper和NestedScrollingChildHelper这两个辅助。所以实现NestedScrolling这两个接口的常用写法是:
ns child:
public class NestedScrollingWebView extends WebView implements NestedScrollingChild { private NestedScrollingChildHelper mChildHelper; private NestedScrollingChildHelper getNestedScrollingHelper() { if (mChildHelper == null) { mChildHelper = new NestedScrollingChildHelper(this); } return mChildHelper; } @Override public void setNestedScrollingEnabled(boolean enabled) { getNestedScrollingHelper().setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return getNestedScrollingHelper().isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return getNestedScrollingHelper().startNestedScroll(axes); } @Override public void stopNestedScroll() { getNestedScrollingHelper().stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return getNestedScrollingHelper().hasNestedScrollingParent(); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) { return getNestedScrollingHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) { return getNestedScrollingHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return getNestedScrollingHelper().dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return getNestedScrollingHelper().dispatchNestedPreFling(velocityX, velocityY); } @Override public boolean startNestedScroll(int axes, int type) { return getNestedScrollingHelper().startNestedScroll(axes, type); } @Override public void stopNestedScroll(int type) { getNestedScrollingHelper().stopNestedScroll(type); } @Override public boolean hasNestedScrollingParent(int type) { return getNestedScrollingHelper().hasNestedScrollingParent(type); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) { return getNestedScrollingHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) { return getNestedScrollingHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); } }
ns parent:
public class NestedScrollingDetailContainer extends ViewGroup implements NestedScrollingParent { private NestedScrollingParentHelper mParentHelper; private NestedScrollingParentHelper getNestedScrollingHelper() { if (mParentHelper == null) { mParentHelper = new NestedScrollingParentHelper(this); } return mParentHelper; } @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public int getNestedScrollAxes() { return getNestedScrollingHelper().getNestedScrollAxes(); } @Override public void onNestedScrollAccepted(View child, View target, int axes) { getNestedScrollingHelper().onNestedScrollAccepted(child, target, axes); } @Override public void onStopNestedScroll(View child) { getNestedScrollingHelper().onStopNestedScroll(child); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { // 处理预先flying事件 return false; } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { // 处理后续flying事件 return false; } @Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { // 处理后续scroll事件 } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed) { // 处理预先滑动scroll事件 } }三、效果实现
只知道原理大家肯定是不满足的,结合原理进行实操才是关键。这里以DEMO的效果为例,想要实现DEMO的效果,需要自定义两个嵌套滑动容器:
自定义一个支持嵌套WebView和RecyclerView滑动的外部容器。
自定义一个实现NestedScrollingChild接口的WebView。
外部滑动容器
在实现外部滑动的容器的时候,我们首先需要考虑这个外部滑动容器的滑动阈值是什么?
答: 外部滑动的滑动阈值=外部容器中所有子View的高度-外部容器的高度。同理类似WebView的滑动阈值=WebView的内容高度-WebView的容器高度。
对应代码实现:
private int mInnerScrollHeight; // 可滑动的最大距离 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { width = measureWidth; } else { width = mScreenWidth; } int left = getPaddingLeft(); int right = getPaddingRight(); int top = getPaddingTop(); int bottom = getPaddingBottom(); int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); LayoutParams params = child.getLayoutParams(); int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, left + right, params.width); int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, top + bottom, params.height); measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); } setMeasuredDimension(width, measureHeight); findWebView(this); findRecyclerView(this); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childTotalHeight = 0; mInnerScrollHeight = 0; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); child.layout(0, childTotalHeight, childWidth, childHeight + childTotalHeight); childTotalHeight += childHeight; mInnerScrollHeight += childHeight; } mInnerScrollHeight -= getMeasuredHeight(); }
其次,需要考虑当WebView传递上滑事件和RecyclerView传递下滑事件时如何处理:
向上滑动时,如果WebView内容还没有到底,该事件交给WebView处理;如果WebView内容已经滑动到底,但是滑动距离没有超过外部容器的最大滑动距离,该事件由滑动容器自身处理;如果WebView内容已经滑动到底,并且滑动距离超过了外部容器的最大滑动距离,这时将滑动事件传递给底部的 RecyclerView,让RecyclerView处理;
向下滑动时,如果RecyclerView没有到顶部,该事件交给RecyclerView处理;如果RecyclerView已经到顶部,并且外部容器的滑动距离不为0,该事件由外部容器处理;如果RecyclerView已经到顶部,并且外部容器的滑动距离已经为0,则该事件交给WebView处理;
对应的WebView传递上滑速度和RecyclerView传递下滑速度,处理和Scroll传递类似。
对应代码实现:
@Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) { boolean isWebViewBottom = !canWebViewScrollDown(); boolean isCenter = isParentCenter(); if (dy > 0 && isWebViewBottom && getScrollY() < getInnerScrollHeight()) { //为了WebView滑动到底部,继续向下滑动父控件 scrollBy(0, dy); if (consumed != null) { consumed[1] = dy; } } else if (dy < 0 && isCenter) { //为了RecyclerView滑动到顶部时,继续向上滑动父控件 scrollBy(0, dy); if (consumed != null) { consumed[1] = dy; } } if (isCenter && !isWebViewBottom) { //异常情况的处理 scrollToWebViewBottom(); } } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { if (target instanceof NestedScrollingWebView) { //WebView滑到底部时,继续向下滑动父控件和RV mCurFlyingType = FLYING_FROM_WEBVIEW_TO_PARENT; parentFling(velocityY); } else if (target instanceof RecyclerView && velocityY < 0 && getScrollY() >= getInnerScrollHeight()) { //RV滑动到顶部时,继续向上滑动父控件和WebView,这里用于计算到达父控件的顶部时RV的速度 mCurFlyingType = FLYING_FROM_RVLIST_TO_PARENT; parentFling((int) velocityY); } else if (target instanceof RecyclerView && velocityY > 0) { mIsRvFlyingDown = true; } return false; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { int currY = mScroller.getCurrY(); switch (mCurFlyingType) { case FLYING_FROM_WEBVIEW_TO_PARENT://WebView向父控件滑动 if (mIsRvFlyingDown) { //RecyclerView的区域的fling由自己完成 break; } scrollTo(0, currY); invalidate(); checkRvTop(); if (getScrollY() == getInnerScrollHeight() && !mIsSetFlying) { //滚动到父控件底部,滚动事件交给RecyclerView mIsSetFlying = true; recyclerViewFling((int) mScroller.getCurrVelocity()); } break; case FLYING_FROM_PARENT_TO_WEBVIEW://父控件向WebView滑动 scrollTo(0, currY); invalidate(); if (currY <= 0 && !mIsSetFlying) { //滚动父控件顶部,滚动事件交给WebView mIsSetFlying = true; webViewFling((int) -mScroller.getCurrVelocity()); } break; case FLYING_FROM_RVLIST_TO_PARENT://RecyclerView向父控件滑动,fling事件,单纯用于计算速度。RecyclerView的flying事件传递最终会转换成Scroll事件处理. if (getScrollY() != 0) { invalidate(); } else if (!mIsSetFlying) { mIsSetFlying = true; //滑动到顶部时,滚动事件交给WebView webViewFling((int) -mScroller.getCurrVelocity()); } break; } } }
最后,我们需要考虑,如果用户触摸的是内部的一个不可滑动View时,这时事件是没法通过NestedScrolling机制传递给自身的。所以需要主动拦截这种事件,拦截标准:
MOVE超过的TOUCH SLOP距离。
当前触摸的不是支持NestedScrolling机制的View。
相应代码如下:
private int mLastY; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mLastY = (int) event.getY(); break; case MotionEvent.ACTION_MOVE: if (mLastY == 0) { mLastY = (int) event.getY(); return true; } int y = (int) event.getY(); int dy = y - mLastY; mLastY = y; scrollBy(0, -dy); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mLastY = 0; break; } return true; } private int mLastMotionY; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: // 拦截落在不可滑动子View的MOVE事件 final int y = (int) ev.getY(); final int yDiff = Math.abs(y - mLastMotionY); boolean isInNestedChildViewArea = isTouchNestedInnerView((int)ev.getRawX(), (int)ev.getRawY()); if (yDiff > TOUCH_SLOP && !isInNestedChildViewArea) { mIsBeingDragged = true; mLastMotionY = y; final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; case MotionEvent.ACTION_DOWN: mLastMotionY = (int) ev.getY(); mIsBeingDragged = false; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsBeingDragged = false; break; } return mIsBeingDragged; } private boolean isTouchNestedInnerView(int x, int y) { ListinnerView = new ArrayList<>(); if (mChildWebView != null) { innerView.add(mChildWebView); } if (mChildRecyclerView != null) { innerView.add(mChildRecyclerView); } for (View nestedView : innerView) { if (nestedView.getVisibility() != View.VISIBLE) { continue; } int[] location = new int[2]; nestedView.getLocationOnScreen(location); int left = location[0]; int top = location[1]; int right = left + nestedView.getMeasuredWidth(); int bottom = top + nestedView.getMeasuredHeight(); if (y >= top && y <= bottom && x >= left && x <= right) { return true; } } return false; }
实现一个支持嵌套滑动的WebView
本身WebView是不支持嵌套滑动的,想要支持嵌套滑动,我们需要让WebView实现NestedScrollingChild接口,并且处理好TouchEvent方法中的事件传递。
实现NestedScrollingChild接口比较简单,上面也介绍过了,可以使用Google提供的NestedScrollingChildHelper辅助类。
处理TouchEvent的思路,需要遵循以下步骤:
DOWN事件时通知父布局,自己要开始嵌套滑动了。
对于MOVE事件,先交给父布局消费。父布局判断WebView不能向下滑动了,就父布局消费;还能向下滑动,就给WebView消费。
对于Flying事件,同样先咨询父布局是否消费。父布局判断WebView不能向下滑动了,就父布局消费;还能向下滑动,就给WebView消费。
WebView最大滑动距离=WebView自身内容的高度-WebView容器的高度
思路比较简单,我们看一下对应的核心代码:
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mWebViewContentHeight = 0; mLastY = (int) event.getRawY(); mFirstY = mLastY; if (!mScroller.isFinished()) { mScroller.abortAnimation(); } initOrResetVelocityTracker(); mIsSelfFling = false; mHasFling = false; mMaxScrollY = getWebViewContentHeight() - getHeight(); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); } break; case MotionEvent.ACTION_MOVE: initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(event); int y = (int) event.getRawY(); int dy = y - mLastY; mLastY = y; if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); } if (!dispatchNestedPreScroll(0, -dy, mScrollConsumed, null)) { scrollBy(0, -dy); } if (Math.abs(mFirstY - y) > TOUCHSLOP) { //屏蔽WebView本身的滑动,滑动事件自己处理 event.setAction(MotionEvent.ACTION_CANCEL); } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: if (isParentResetScroll() && mVelocityTracker != null) { mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int yVelocity = (int) -mVelocityTracker.getYVelocity(); recycleVelocityTracker(); mIsSelfFling = true; flingScroll(0, yVelocity); } break; } super.onTouchEvent(event); return true; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { final int currY = mScroller.getCurrY(); if (!mIsSelfFling) { // parent flying scrollTo(0, currY); invalidate(); return; } if (isWebViewCanScroll()) { scrollTo(0, currY); invalidate(); } if (!mHasFling && mScroller.getStartY() < currY && !canScrollDown() && startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) && !dispatchNestedPreFling(0, mScroller.getCurrVelocity())) { //滑动到底部时,将fling传递给父控件和RecyclerView mHasFling = true; dispatchNestedFling(0, mScroller.getCurrVelocity(), false); } } }总结
NestedScrolling机制看似复杂,但其实就是实现两个接口的事情,而且Google提供了强大的辅助类Helper来帮助我们实现接口。这种机制将滑动事件的传递封装起来,通过Helper辅助类实现ns parent和ns child之间的连接和交互。通过接口回调,也实现了ns parent和ns child的解耦。
DEMO项目链接:Android-NestedDetail
参考链接Android NestedScrolling机制完全解析 带你玩转嵌套滑动
NestedScrolling:文章详情页的实现
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/14972.html
摘要:那些酷炫的开源库整理整理开源库,文章提交,欢迎提交更新二维可拖动面板控件控件可用于房态盘计划表待办事项课程表等相关需要二维视图的场合,需要的可以收藏下五分钟带你看懂嵌套滑动机制在发布之后加入了嵌套滑动机制为嵌套滑动提供了更方便的处理方案。 仿淘宝、京东拖拽商品详情(可嵌套ViewPager、ListView、WebView、FragmentTabhost) 对于电商App,商品详情无疑...
摘要:分析提到侧滑删除,一个经典的例子就是玩转仿探探卡片式滑动效果掘金讲起本篇博客的历史起源,估计有一段历史了。列表左右滑动开源组件掘金是一款用于为上的排布提供左滑右滑操作的库。 Android 仿 YouTube 拖拽视频效果的实现 - Android - 掘金Android仿YouTube拖拽视频效果的实现 youtube-like-drag-video-view 代码已经开源到GitH...
摘要:同时我们或许还会有这样的体验打开联系人界面,手指向上滑动,联系人列表也会跟着一动态权限申请步骤以及需要注意的一些坑掘金因为工作需要,简单研究了一下权限申请,在提供的的基础上,写了一个简单的。 自定义未读消息红点提示 - Android - 掘金未读消息提示,可自定义颜色及Padding 效果展示... Android 实现底部对话框 - Android - 掘金最近项目上需要实现一个底...
摘要:再次感谢在发布版本后,为了更好的用户体验,为的滑动机制提供了机制。如图在这之前,我们知道对事件分发是有自己的一套机制。主要是有三个函数。简单逻辑这样就可以实现嵌套滑动。三向父汇报滚动情况,包括子已消费和未消费的值。四结束整个嵌套滑动流程。 终于要在segmentfault写第一篇文章了,好鸡冻,这篇文章本来打算写在简书上的,但是由于页面不能富文本和markdown同时支持,看到Gemi...
摘要:效果如上图别嫌弃我在这之前,我们知道对事件的分发是有自己一套机制的。主要是有是三个函数和。该方法的第三第四个参数返回父消费掉的长度和子的窗体偏移量。三向父汇报滚动情况,包括子消费的部分和子没有消费的部分。 Android 在发布 Lollipop版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性 NestedScrol...
阅读 3123·2023-04-25 22:45
阅读 1084·2021-11-11 16:54
阅读 2678·2019-08-30 15:44
阅读 3091·2019-08-30 15:44
阅读 1531·2019-08-30 13:55
阅读 832·2019-08-29 18:45
阅读 1086·2019-08-29 17:25
阅读 917·2019-08-29 12:59