Android应用ViewDragHelper详解及部分源码浅析,踩坑了

public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {

//mDrawerView完全挪出屏幕则防止过度绘制

mDrawerView.setVisibility((changedView.getHeight()+top == 0)? View.GONE : View.VISIBLE);

mCurTop = top;

requestLayout();

}

@Override

public int getViewVerticalDragRange(View child) {

if (mDrawerView == null) return 0;

return (mDrawerView == child) ? mDrawerView.getHeight() : 0;

}

@Override

public void onViewDragStateChanged(int state) {

super.onViewDragStateChanged(state);

if (state == ViewDragHelper.STATE_IDLE) {

mIsOpen = (mDrawerView.getTop() == 0);

}

}

}

@Override

public void computeScroll() {

if (mTopViewDragHelper.continueSettling(true)) {

invalidate();

}

}

public void closeDrawer() {

if (mIsOpen) {

mTopViewDragHelper.smoothSlideViewTo(mDrawerView, mDrawerView.getLeft(), -mDrawerView.getHeight());

invalidate();

}

}

public void openDrawer() {

if (!mIsOpen) {

mTopViewDragHelper.smoothSlideViewTo(mDrawerView, mDrawerView.getLeft(), 0);

invalidate();

}

}

public boolean isDrawerOpened() {

return mIsOpen;

}

//Step3:重写onInterceptTouchEvent回调ViewDragHelper中对应的方法.

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

return mTopViewDragHelper.shouldInterceptTouchEvent(ev);

}

//Step3:重写onTouchEvent回调ViewDragHelper中对应的方法.

@Override

public boolean onTouchEvent(MotionEvent event) {

mTopViewDragHelper.processTouchEvent(event);

return true;

}

@Override

protected LayoutParams generateDefaultLayoutParams() {

return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

}

@Override

protected LayoutParams generateLayoutParams(LayoutParams p) {

return new MarginLayoutParams§;

}

@Override

public LayoutParams generateLayoutParams(AttributeSet attrs) {

return new MarginLayoutParams(getContext(), attrs);

}

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int measureWidth = MeasureSpec.getSize(widthMeasureSpec);

int measureHeight = MeasureSpec.getSize(heightMeasureSpec);

setMeasuredDimension(measureWidth, measureHeight);

mContentView = getChildAt(0);

mDrawerView = getChildAt(1);

MarginLayoutParams params = (MarginLayoutParams) mContentView.getLayoutParams();

int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(

measureWidth- (params.leftMargin + params.rightMargin), MeasureSpec.EXACTLY);

int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(

measureHeight - (params.topMargin + params.bottomMargin), MeasureSpec.EXACTLY);

mContentView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

mDrawerView.measure(widthMeasureSpec, heightMeasureSpec);

}

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

if (changed) {

MarginLayoutParams params = (MarginLayoutParams) mContentView.getLayoutParams();

mContentView.layout(params.leftMargin, params.topMargin,

mContentView.getMeasuredWidth() + params.leftMargin,

mContentView.getMeasuredHeight() + params.topMargin);

params = (MarginLayoutParams) mDrawerView.getLayoutParams();

mDrawerView.layout(params.leftMargin, mCurTop + params.topMargin,

mDrawerView.getMeasuredWidth() + params.leftMargin,

mCurTop + mDrawerView.getMeasuredHeight() + params.topMargin);

}

}

}

怎么样,简单吧。效果也有了,代码也有了,ViewDragHelper也体验了,接下来就该苦逼的看源码了。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

3 ViewDragHelper局部源码浅析

==========================

上面的例子中我们可以知道,使用ViewDragHelper的第一步就是通过他提供的静态工厂方法create获取实例,因为ViewDragHelper的构造方法是私有的。既然这样那我们先看下这些静态工厂方法,如下:

public class ViewDragHelper {

public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {

final ViewDragHelper helper = create(forParent, cb);

helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));

return helper;

}

public static ViewDragHelper create(ViewGroup forParent, Callback cb) {

return new ViewDragHelper(forParent.getContext(), forParent, cb);

}

}

可以看见,三个参数的create方法实质调运的还是两个参数的create。其中forParent一般是我们自定义的ViewGroup,cb是控制子View相关状态的回调抽象类实现对象,sensitivity是用来调节mTouchSlop的,至于mTouchSlop是啥以及sensitivity的作用下面会有解释。接着可以发现两个参数的create实质是调运了ViewDragHelper的构造函数,那我们就来分析一下这个构造函数,如下源码:

private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {

//对参数进行赋值

mParentView = forParent;

mCallback = cb;

//通过ViewConfiguration等将dp转px得到mEdgeSize

final ViewConfiguration vc = ViewConfiguration.get(context);

final float density = context.getResources().getDisplayMetrics().density;

mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);

//通过ViewConfiguration获取TouchSlop,默认为8

mTouchSlop = vc.getScaledTouchSlop();

//获得允许执行一个fling手势动作的最大速度值

mMaxVelocity = vc.getScaledMaximumFlingVelocity();

//获得允许执行一个fling手势动作的最小速度值

mMinVelocity = vc.getScaledMinimumFlingVelocity();

//通过兼容包的ScrollerCompat实例化Scroller,动画插值器为sInterpolator

mScroller = ScrollerCompat.create(context, sInterpolator);

}

可以看见,构造函数其实没有做啥特别的事情,主要就是一些参数的实例化,最主要的就是实例化了一个Scroller的内部成员,而且还在ViewDragHelper中重写了插值器,如下:

private static final Interpolator sInterpolator = new Interpolator() {

public float getInterpolation(float t) {

t -= 1.0f;

return t * t * t * t * t + 1.0f;

}

};

关于动画插值器这里不再说了,之前博文有讲过。我们还是把视线回到上面实例部分,可以看见,在获取ViewDragHelper实例之后我们接着重写了ViewGroup的onInterceptTouchEvent和onTouchEvent方法,在其中触发了ViewDragHelper的shouldInterceptTouchEvent和processTouchEvent方法。所以下面我们就来分析这两个方法,首先我们看下shouldInterceptTouchEvent方法,如下:

//这玩意返回值的作用在前面博客中有分析,我们先来看下ACTION_DOWN事件

public boolean shouldInterceptTouchEvent(MotionEvent ev) {

final int action = MotionEventCompat.getActionMasked(ev);

final int actionIndex = MotionEventCompat.getActionIndex(ev);

if (action == MotionEvent.ACTION_DOWN) {

//每次ACTION_DOWN都会调用cancel(),该方法中mVelocityTracker被清空,故mVelocityTracker记录的是本次ACTION_DOWN到ACTION_UP的触摸信息

cancel();

}

//获取VelocityTracker实例,记录下各个触摸点信息用来计算本次滑动速率等

if (mVelocityTracker == null) {

mVelocityTracker = VelocityTracker.obtain();

}

mVelocityTracker.addMovement(ev);

switch (action) {

case MotionEvent.ACTION_DOWN: {

final float x = ev.getX();

final float y = ev.getY();

final int pointerId = MotionEventCompat.getPointerId(ev, 0);

//Step 1

saveInitialMotion(x, y, pointerId);

//Step 2

final View toCapture = findTopChildUnder((int) x, (int) y);

//Step 3

if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {

tryCaptureViewForDrag(toCapture, pointerId);

}

//Step 4

final int edgesTouched = mInitialEdgesTouched[pointerId];

if ((edgesTouched & mTrackingEdges) != 0) {

mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);

}

break;

}

//暂时忽略

}

//Step 5

return mDragState == STATE_DRAGGING;

}

可以看见,上面代码我们只列出了shouldInterceptTouchEvent关于ACTION_DOWN部分的代码,在注释里我将他分为了5步来叙述。我们先来看当ACTION_DOWN触发时Step 1的代码,他通过saveInitialMotion(x, y, pointerId)保存了事件的初始信息,如下是saveInitialMotion方法代码:

private void saveInitialMotion(float x, float y, int pointerId) {

ensureMotionHistorySizeForId(pointerId);

mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;

mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;

//getEdgesTouched就是通过mEdgeSize去判断触摸边沿方向是否OK

mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);

mPointersDown |= 1 << pointerId;

}

接着目光再回到shouldInterceptTouchEvent方法的Step 2,可以发现他尝试通过findTopChildUnder()方法来获取当前触摸点下最顶层的子View,我们可以看下这个方法的源码,如下:

public View findTopChildUnder(int x, int y) {

//获取mParentView中子View个数

final int childCount = mParentView.getChildCount();

//倒序遍历整个子View,因为最上面的子View最后插入

for (int i = childCount - 1; i >= 0; i–) {

//遍历拿到最靠上且获得触摸焦点的那个子View

final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));

//判断当前DOWN的触摸点是否在该子View范围,也就是说是不是摸上了该子View

if (x >= child.getLeft() && x < child.getRight() &&

y >= child.getTop() && y < child.getBottom()) {

return child;

}

}

return null;

}

这个方法你不觉的奇怪么?怎么遍历时getChildAt传入的不是index,而是mCallback.getOrderedChildIndex(i)啊,我勒个去,mCallback不就是我们创建ViewDragHelper实例时传入的CallBack对象么?我们还是跳过去看下getOrderedChildIndex()方法吧,如下:

public static abstract class Callback {

public int getOrderedChildIndex(int index) {

return index;

}

}

猫了个咪的,这玩意默认啥也没干啊,看样子是某些情况下给我们用来重写的,啥情况?原来在上面findTopChildUnder()方法中返回当前被触摸的View时会有一种坑爹的情况出现,那就是如果在mParentView的同一个位置有多个子View是重叠的,此时又想让重叠的View中下面指定的那个被选中(默认for循环是倒序额)时getOrderedChildIndex()方法的默认实现就搞不定了,所以就需要我们自己去实现Callback里的getOrderedChildIndex()方法来改变查找子View的顺序。譬如:

public int getOrderedChildIndex(int index) {

//实现重叠View时让下面的View获得选中

int topIndex = mParentView.indexOfChild(your_top_view);

int BottomSelectedIndex = mParentView.indexOfChild(blow_your_top_view_selected);

return ((index == topIndex) ? indexBottom : index);

}

好了,扯远了,我们还是把目光回到shouldInterceptTouchEvent方法的Step 3,可以发现这里有一个判断,因为第一次触摸屏幕mCapturedView默认为null,所以一开始不会执行这个判断里的代码,同时因为mDragState第一次也不处于STATE_SETTLING状态,所以不执行,等执行时再分析。我们继续往下看Step 4,可以发现这里首先拿了saveInitialMotion方法赋值的结果,然后判断设置的边沿方向进行Callback的onEdgeTouched()方法回调。到这里boolean shouldInterceptTouchEvent()方法的第一次触摸按下ACTION_DOWN所干的事就完了,接着Step 5时直接return了mDragState == STATE_DRAGGING;,因为上面说了,在ACTION_DOWN时mDragState还是STATE_IDLE状态,所以这里返回了false。

至此第一次手指触摸mParentView上时shouldInterceptTouchEvent的ACTION_DOWN流程就结束了,接着我们就是依据这个返回值的情况进行分析(具体参见之前博文关于Android触摸事件传递的分析)。这里返回false就表示mParentView没有拦截这次事件,所以接下来会在mParentView中触发每个子View的boolean dispatchTouchEvent()方法,这时依据Android触摸事件处理机制又分为了两大类情况来处理,一类就是子View消费了这个ACTION_DOWN,一类是没有消费的情况,而这些情况下又分很多中情况,譬如子View消费了本次ACTION_DOWN,mParentView的onTouchEvent()就不会收到ACTION_DOWN了(即ViewDragHelper的processTouchEvent()方法也就收不到ACTION_DOWN了);这时候又有很多情况,譬如当前子View如果调运了requestDisallowInterceptTouchEvent(true),则ACTION_MOVE等来时mParentView的onInterceptTouchEvent()方法就不会被回调(即ViewDragHelper的相关方法也就没意义了);当前子View没有调用requestDisallowInterceptTouchEvent(true),则ACTION_MOVE等来时mParentView的onInterceptTouchEvent()方法还会被执行,此时若onInterceptTouchEvent()方法返回true,则mParentView的onTouchEvent()就会被调运(即ViewDragHelper的processTouchEvent()会被执行)。

额额额,触摸事件传递本来就很复杂,这里情况又很多,所以我们还是不分情况来说了,浅析源码我们牵一条主线走就行,其它的用到时再分析即可。

所以我们来看下子View没有消费这次ACTION_DOWN事件(即子View的dispatchTouchEvent()方法返回false)的流程。此时mParentView的dispatchTouchEvent()方法会调用自己的super.dispatchTouchEvent()方法(即View的dispatchTouchEvent()),然后super会调用mParentView的onTouchEvent()方法(即调用ViewDragHelper的processTouchEvent()方法)。这时onTouchEvent()方法需要返回true(只用在ACTION_DOWN时返回true,否则onTouchEvent()方法无法接收接下来的ACTION_MOVE等事件),当onTouchEvent()返回true以后ACTION_MOVE、ACTION_UP等事件再来时就不会再执行mParentView的onInterceptTouchEvent()了。

那就牵着主线走吧,我们可以看下processTouchEvent()方法的ACTION_DOWN部份源码,如下:

public void processTouchEvent(MotionEvent ev) {

//和shouldInterceptTouchEvent相似,省略

switch (action) {

case MotionEvent.ACTION_DOWN: {

//和shouldInterceptTouchEvent相似,省略解释

final float x = ev.getX();

final float y = ev.getY();

final int pointerId = MotionEventCompat.getPointerId(ev, 0);

final View toCapture = findTopChildUnder((int) x, (int) y);

saveInitialMotion(x, y, pointerId);

//Step 1 重点!!!!

tryCaptureViewForDrag(toCapture, pointerId);

//和shouldInterceptTouchEvent相似,省略解释

final int edgesTouched = mInitialEdgesTouched[pointerId];

if ((edgesTouched & mTrackingEdges) != 0) {

mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);

}

break;

}

//省略其他ACTION

}

}

可以看见,该方法里最核心的东西估计就是tryCaptureViewForDrag()方法了,下面我们看下这个方法,如下:

boolean tryCaptureViewForDrag(View toCapture, int pointerId) {

//正在拽就不管了

if (toCapture == mCapturedView && mActivePointerId == pointerId) {

// Already done!

return true;

}

//调用了Callback的tryCaptureView()方法,传递触摸到的View和触摸点编号

//Callback的tryCaptureView()决定是否能够拖动当前触摸的View

if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {

mActivePointerId = pointerId;

//重点

captureChildView(toCapture, pointerId);

return true;

}

return false;

}

可以看见,通过Callback的tryCaptureView()重写设置是否可以挪动该View,若可以挪动(返回true)则又调运了captureChildView()方法,继续看下captureChildView()方法源码:

public void captureChildView(View childView, int activePointerId) {

//暂存被捕获的这个View的相关信息及触摸信息

mCapturedView = childView;

mActivePointerId = activePointerId;

//通过Callback的onViewCaptured()方法回调当前View被捕获了

mCallback.onViewCaptured(childView, activePointerId);

//设置当前被捕获的子View状态为STATE_DRAGGING

//里面会通过mCallback.onViewDragStateChanged(state)回调告知状态

setDragState(STATE_DRAGGING);

}

到此一次mParentView自己消费事件,子View无拦截ACTION_DOWN的事件处理就彻底结束了。接着就是主流程的ACTION_MOVE事件了,这玩意由于mParentView的onTouchEvent消费了事件且没进行拦截ACTION_DOWN,所以一旦触发时就直接走进了processTouchEvent()方法里,下面是ACTION_MOVE代码:

public void processTouchEvent(MotionEvent ev) {

switch (action) {

case MotionEvent.ACTION_MOVE: {

//分两种情况,依赖上一个ACTION_DOWN事件

if (mDragState == STATE_DRAGGING) {

//ACTION_DOWN时CallBack的tryCaptureView()返回true时对mDragState赋值了STATE_DRAGGING,故此流程

final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);

final float x = MotionEventCompat.getX(ev, index);

final float y = MotionEventCompat.getY(ev, index);

final int idx = (int) (x - mLastMotionX[mActivePointerId]);

final int idy = (int) (y - mLastMotionY[mActivePointerId]);

//Step 1 重点!!!!!!

dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);

//Step 2 重点!!!!!!

saveLastMotion(ev);

} else {

//ACTION_DOWN时CallBack的tryCaptureView()返回false时对mDragState没进行赋值,故此流程

// Check to see if any pointer is now over a draggable view.

final int pointerCount = MotionEventCompat.getPointerCount(ev);

for (int i = 0; i < pointerCount; i++) {

final int pointerId = MotionEventCompat.getPointerId(ev, i);

final float x = MotionEventCompat.getX(ev, i);

final float y = MotionEventCompat.getY(ev, i);

final float dx = x - mInitialMotionX[pointerId];

final float dy = y - mInitialMotionY[pointerId];

//Step 3 重点!!!!!!

reportNewEdgeDrags(dx, dy, pointerId);

if (mDragState == STATE_DRAGGING) {

// Callback might have started an edge drag.

break;

}

final View toCapture = findTopChildUnder((int) x, (int) y);

//Step 4 重点!!!!!!

if (checkTouchSlop(toCapture, dx, dy) &&

tryCaptureViewForDrag(toCapture, pointerId)) {

break;

}

}

saveLastMotion(ev);

}

break;

}

}

}

可以看见,当ACTION_MOVE事件多次触发时该段代码会依据我们重写CallBack的代码分为可以托拽当前View和不能托拽两种情况。

我们先来看下不能托拽的情况,这种相对比较简单,可以看到上面代码的Step 3部分,reportNewEdgeDrags()方法是我们的重点,如下:

//在托拽时该方法会被多次调运

private void reportNewEdgeDrags(float dx, float dy, int pointerId) {

int dragsStarted = 0;

…//四个方向,省略三个

if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {

dragsStarted |= EDGE_BOTTOM;

}

if (dragsStarted != 0) {

mEdgeDragsInProgress[pointerId] |= dragsStarted;

//该方法只会被调运一次,checkNewEdgeDrag方法中有处理

mCallback.onEdgeDragStarted(dragsStarted, pointerId);

}

}

可以发现,当我们在ACTION_DOWN触发时重写CallBack的tryCaptureView()方法返回false(当前View不能托拽)且是边沿触摸时移动时首先会回调Callback的onEdgeDragStarted()方法通知自定义ViewGroup开始边沿托拽。接着我们把目光投向Step 4部分,if (checkTouchSlop(toCapture, dx, dy) && tryCaptureViewForDrag(toCapture, pointerId)) 判断是我们关注的核心,toCapture其实就是当前捕获的View(这个View在边沿模式时一般摸不到,所以其实拿到的不是想要的childView,所以一般我们会在回调onEdgeDragStarted()方法中重写手动调用captureChildView()方法,传入我们摸不到的View,这样就相当于绕过tryCaptureView将状态设置为STATE_DRAGGING了),下面我们先看下checkTouchSlop()方法,如下:

//检查手指移动的距离有没有超过触发处理移动事件的最短距离mTouchSlop

private boolean checkTouchSlop(View child, float dx, float dy) {

if (child == null) {

return false;

}

//如果想让某个View滑动,就要返回大于0,否则processTouchEvent()的ACTION_MOVE就不会调用tryCaptureViewForDrag()来捕获当前触摸的View

final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;

final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;

if (checkHorizontal && checkVertical) {

return dx * dx + dy * dy > mTouchSlop * mTouchSlop;

} else if (checkHorizontal) {

return Math.abs(dx) > mTouchSlop;

} else if (checkVertical) {

return Math.abs(dy) > mTouchSlop;

}

return false;

}

到此ACTION_MOVE不能托拽的情况就分析完毕了,我们再来看下可以托拽的情况,请看上面processTouchEvent()代码的Step 1,2部分,重点在于dragTo()方法,当我们正常捕获到View时ACTION_MOVE就不停的调用dragTo()对mCaptureView进行拖动,源码如下:

//left、top为mCapturedView.getLeft()+dx、mCapturedView.getTop()+dy,即期望目标坐标

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

《960全网最全Android开发笔记》

《379页Android开发面试宝典》

《507页Android开发相关源码解析》

部分,重点在于dragTo()方法,当我们正常捕获到View时ACTION_MOVE就不停的调用dragTo()对mCaptureView进行拖动,源码如下:

//left、top为mCapturedView.getLeft()+dx、mCapturedView.getTop()+dy,即期望目标坐标

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-Vzf3ohjp-1711352102626)]
[外链图片转存中…(img-jkfY2AK1-1711352102627)]
[外链图片转存中…(img-PKzVgCKe-1711352102627)]
[外链图片转存中…(img-DdcqLD08-1711352102628)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-CYXPFqZH-1711352102628)]

《960全网最全Android开发笔记》

[外链图片转存中…(img-rhrVNThO-1711352102629)]

《379页Android开发面试宝典》

[外链图片转存中…(img-KBRHgugp-1711352102630)]

《507页Android开发相关源码解析》

[外链图片转存中…(img-rqq3qrlA-1711352102630)]

因为文件太多,全部展示会影响篇幅,暂时就先列举这些部分截图,大家可以**点击这里**自行领取。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值