2024年Android最全Android:让你明明白白的使用RecyclerView——SnapHelper详解(4),2024年最新面试建议英语

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

Android进阶学习资料库

一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

该方法是返回第二个传参对应的view到RecyclerView中间位置的距离,可以支持水平方向滚动和竖直方向滚动两个方向的计算。最主要的计算距离的这个方法distanceToCenter()

private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
//找到targetView的中心坐标
final int childCenter = helper.getDecoratedStart(targetView) +
(helper.getDecoratedMeasurement(targetView) / 2);
final int containerCenter;
//找到容器(RecyclerView)的中心坐标
if (layoutManager.getClipToPadding()) {
containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
} else {
containerCenter = helper.getEnd() / 2;
}
//两个中心坐标的差值就是targetView需要滚动的距离
return childCenter - containerCenter;
}

可以看到,就是计算对应的view的中心坐标到RecyclerView中心坐标之间的距离,该距离就是此view需要滚动的距离。

findSnapView()

@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
}
return null;
}

寻找SnapView,这里的目的坐标就是RecyclerView中间位置坐标,可以看到会根据layoutManager的布局方式(水平布局方式或者竖向布局方式)区分计算,但最终都是通过findCenterView()方法来找snapView的。

private View findCenterView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}

View closestChild = null;
//找到RecyclerView的中心坐标
final int center;
if (layoutManager.getClipToPadding()) {
center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
} else {
center = helper.getEnd() / 2;
}
int absClosest = Integer.MAX_VALUE;

//遍历当前layoutManager中所有的ItemView
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
//ItemView的中心坐标
int childCenter = helper.getDecoratedStart(child) +
(helper.getDecoratedMeasurement(child) / 2);
//计算此ItemView与RecyclerView中心坐标的距离
int absDistance = Math.abs(childCenter - center);

//对比每个ItemView距离到RecyclerView中心点的距离,找到那个最靠近中心的ItemView然后返回
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}

注释解释得很清楚,就不重复了。

findTargetSnapPosition()

@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
//判断layoutManager是否实现了RecyclerView.SmoothScroller.ScrollVectorProvider这个接口
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return RecyclerView.NO_POSITION;
}

final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}

//找到snapView
final View currentView = findSnapView(layoutManager);
if (currentView == null) {
return RecyclerView.NO_POSITION;
}

final int currentPosition = layoutManager.getPosition(currentView);
if (currentPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}

RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
// 通过ScrollVectorProvider接口中的computeScrollVectorForPosition()方法
// 来确定layoutManager的布局方向
PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
if (vectorForEnd == null) {
return RecyclerView.NO_POSITION;
}

int vDeltaJump, hDeltaJump;
if (layoutManager.canScrollHorizontally()) {
//layoutManager是横向布局,并且内容超出一屏,canScrollHorizontally()才返回true
//估算fling结束时相对于当前snapView位置的横向位置偏移量
hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getHorizontalHelper(layoutManager), velocityX, 0);
//vectorForEnd.x < 0代表layoutManager是反向布局的,就把偏移量取反
if (vectorForEnd.x < 0) {
hDeltaJump = -hDeltaJump;
}
} else {
//不能横向滚动,横向位置偏移量当然就为0
hDeltaJump = 0;
}

//竖向的原理同上
if (layoutManager.canScrollVertically()) {
vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getVerticalHelper(layoutManager), 0, velocityY);
if (vectorForEnd.y < 0) {
vDeltaJump = -vDeltaJump;
}
} else {
vDeltaJump = 0;
}

//根据layoutManager的横竖向布局方式,最终横向位置偏移量和竖向位置偏移量二选一,作为fling的位置偏移量
int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
if (deltaJump == 0) {
return RecyclerView.NO_POSITION;
}
//当前位置加上偏移位置,就得到fling结束时的位置,这个位置就是targetPosition
int targetPos = currentPosition + deltaJump;
if (targetPos < 0) {
targetPos = 0;
}
if (targetPos >= itemCount) {
targetPos = itemCount - 1;
}
return targetPos;
}

RecyclerView的layoutManager很灵活,有两种布局方式(横向布局和纵向布局),每种布局方式有两种布局方向(正向布局和反向布局)。这个方法在计算targetPosition的时候把布局方式和布局方向都考虑进去了。布局方式可以通过layoutManager.canScrollHorizontally()/layoutManager.canScrollVertically()来判断,布局方向就通过RecyclerView.SmoothScroller.ScrollVectorProvider这个接口中的computeScrollVectorForPosition()方法来判断。

所以SnapHelper为了适配layoutManager的各种情况,特意要求**只有实现了RecyclerView.SmoothScroller.ScrollVectorProvider接口的layoutManager才能使用SnapHelper进行辅助滚动对齐。**官方提供的LinearLayoutManager、GridLayoutManager和StaggeredGridLayoutManager都实现了这个接口,所以都支持SnapHelper。

这几个方法在计算位置的时候用的是OrientationHelper这个工具类,它是LayoutManager用于测量child的一个辅助类,可以根据Layoutmanager的布局方式和布局方向来计算得到ItemView的大小位置等信息。

从源码中可以看到findTargetSnapPosition()会先找到fling操作被触发时界面上的snapView(因为findTargetSnapPosition()方法是在onFling()方法中被调用的),得到对应的snapPosition,然后通过estimateNextPositionDiffForFling()方法估算位置偏移量,snapPosition加上位置偏移量就得到最终滚动结束时的位置,也就是targetSnapPosition。

这里有一个点需要注意一下,就是在找targetSnapPosition之前是需要先找一个参考位置的,该参考位置就是snapPosition了。这是因为当前界面上不同的ItemView位置相差比较大,用snapPosition作参考位置,会使得参考位置加上位置偏移量得到的targetSnapPosition最接近目的坐标位置,从而让后续的坐标对齐调整更加自然。

看下estimateNextPositionDiffForFling()方法怎么估算位置偏移量的:

private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper, int velocityX, int velocityY) {
//计算滚动的总距离,这个距离受到触发fling时的速度的影响
int[] distances = calculateScrollDistance(velocityX, velocityY);
//计算每个ItemView的长度
float distancePerChild = computeDistancePerChild(layoutManager, helper);
if (distancePerChild <= 0) {
return 0;
}
//这里其实就是根据是横向布局还是纵向布局,来取对应布局方向上的滚动距离
int distance =
Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
//distance的正负值符号表示滚动方向,数值表示滚动距离。横向布局方式,内容从右往左滚动为正;竖向布局方式,内容从下往上滚动为正
// 滚动距离/item的长度=滚动item的个数,这里取计算结果的整数部分
if (distance > 0) {
return (int) Math.floor(distance / distancePerChild);
} else {
return (int) Math.ceil(distance / distancePerChild);
}
}

可以看到就是用滚动总距离除以itemview的长度,从而估算得到需要滚动的item数量,此数值就是位置偏移量。而滚动距离是通过SnapHelper的calculateScrollDistance()方法得到的,ItemView的长度是通过computeDistancePerChild()方法计算出来。

看下这两个方法:

private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
View minPosView = null;
View maxPosView = null;
int minPos = Integer.MAX_VALUE;
int maxPos = Integer.MIN_VALUE;
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return INVALID_DISTANCE;
}

//循环遍历layoutManager的itemView,得到最小position和最大position,以及对应的view
for (int i = 0; i < childCount; i++) {
View child = layoutManager.getChildAt(i);
final int pos = layoutManager.getPosition(child);
if (pos == RecyclerView.NO_POSITION) {
continue;
}
if (pos < minPos) {
minPos = pos;
minPosView = child;
}
if (pos > maxPos) {
maxPos = pos;
maxPosView = child;
}
}
if (minPosView == null || maxPosView == null) {
return INVALID_DISTANCE;
}
//最小位置和最大位置肯定就是分布在layoutManager的两端,但是无法直接确定哪个在起点哪个在终点(因为有正反向布局)
//所以取两者中起点坐标小的那个作为起点坐标
//终点坐标的取值一样的道理
int start = Math.min(helper.getDecoratedStart(minPosView),
helper.getDecoratedStart(maxPosView));
int end = Math.max(helper.getDecoratedEnd(minPosView),
helper.getDecoratedEnd(maxPosView));
//终点坐标减去起点坐标得到这些itemview的总长度
int distance = end - start;
if (distance == 0) {
return INVALID_DISTANCE;
}
// 总长度 / itemview个数 = itemview平均长度
return 1f * distance / ((maxPos - minPos) + 1);
}

可以发现computeDistancePerChild()方法也用总长度除以ItemView个数的方式来得到ItemView平均长度,并且也支持了layoutManager不同的布局方式和布局方向。

public int[] calculateScrollDistance(int velocityX, int velocityY) {
int[] outDist = new int[2];
//mGravityScroller是一个Scroller,通过fling()方法模拟fling操作,通过将起点位置都置为0,此时得到的终点位置就是滚动的距离
mGravityScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
outDist[0] = mGravityScroller.getFinalX();
outDist[1] = mGravityScroller.getFinalY();
return outDist;
}

calculateScrollDistance()是SnapHelper中的方法,它使用到的mGravityScroller是一个在attachToRecyclerView()中初始化的Scroller对象,通过Scroller.fling()方法模拟fling操作,将fling的起点位置为设置为0,此时得到的终点位置就是fling的距离。这个距离会有正负符号之分,表示滚动的方向。

现在明白了吧,LinearSnapHelper的主要功能就是通过实现SnapHelper的三个抽象方法,从而实现辅助RecyclerView滚动Item对齐中心位置。

自定义SnapHelper

经过了以上分析,了解了SnapHelper的工作原理之后,自定义SnapHelper也就更加自如了。现在来看下Google Play主界面的效果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到该效果是一个类似Gallery的横向列表滑动控件,很明显可以用RecyclerView来实现,而滚动后的ItemView是对齐RecyclerView的左边缘位置,这种对齐效果当仍不让就使用了SnapHelper来实现了。这里就主要讲下这个SnapHelper怎么实现的。

创建一个GallerySnapHelper继承SnapHelper实现它的三个抽象方法:

  • calculateDistanceToFinalSnap():计算SnapView当前位置与目标位置的距离

@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}
return out;
}
//targetView的start坐标与RecyclerView的paddingStart之间的差值
//就是需要滚动调整的距离
private int distanceToStart(View targetView, OrientationHelper helper) {
return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
}

  • findSnapView():找到当前时刻的SnapView。

@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
return findStartView(layoutManager, getHorizontalHelper(layoutManager));
}

private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
if (layoutManager instanceof LinearLayoutManager) {
//找出第一个可见的ItemView的位置
int firstChildPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
if (firstChildPosition == RecyclerView.NO_POSITION) {
return null;
}
//找到最后一个完全显示的ItemView,如果该ItemView是列表中的最后一个
//就说明列表已经滑动最后了,这时候就不应该根据第一个ItemView来对齐了
//要不然由于需要跟第一个ItemView对齐最后一个ItemView可能就一直无法完全显示,
//所以这时候直接返回null表示不需要对齐
if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition() == layoutManager.getItemCount() - 1) {
return null;
}

View firstChildView = layoutManager.findViewByPosition(firstChildPosition);
//如果第一个ItemView被遮住的长度没有超过一半,就取该ItemView作为snapView
//超过一半,就把下一个ItemView作为snapView
if (helper.getDecoratedEnd(firstChildView) >= helper.getDecoratedMeasurement(firstChildView) / 2 && helper.getDecoratedEnd(firstChildView) > 0) {
return firstChildView;
} else {
return layoutManager.findViewByPosition(firstChildPosition + 1);
}
} else {
return null;
}
}

  • findTargetSnapPosition(): 在触发fling时找到targetSnapPosition。

@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return RecyclerView.NO_POSITION;
}

final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}

final View currentView = findSnapView(layoutManager);
if (currentView == null) {
return RecyclerView.NO_POSITION;
}

final int currentPosition = layoutManager.getPosition(currentView);
if (currentPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}

RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;

PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
if (vectorForEnd == null) {
return RecyclerView.NO_POSITION;
}

int deltaJump;
if (layoutManager.canScrollHorizontally()) {
deltaJump = estimateNextPositionDiffForFling(layoutManager,
getHorizontalHelper(layoutManager), velocityX, 0);
if (vectorForEnd.x < 0) {
deltaJump = -deltaJump;
}
} else {
deltaJump = 0;
}

if (deltaJump == 0) {
return RecyclerView.NO_POSITION;
}
int targetPos = currentPosition + deltaJump;
if (targetPos < 0) {
targetPos = 0;
}
if (targetPos >= itemCount) {
targetPos = itemCount - 1;
}
return targetPos;
}

这个方法跟LinearSnapHelper的实现基本是一样的。

就这样实现三个抽象方法之后看下效果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

发现基本能像Google Play那样进行对齐左侧边缘。但作为一个有理想有文化有追求的程序员,怎么可以那么容易满足呢?!极致才是最终的目标!没时间解释了,快上车!

目前的效果跟Google Play中的效果主要还有两个差异:

  • 滚动速度明显慢于Google Play的横向列表滚动速度,导致滚动起来感觉比较拖沓,看起来不是很干脆的样子。

  • Google Play那个横向列表一次滚动的个数最多就是一页的Item个数,而目前的效果滑得比较快时会滚得很远。

其实这两个问题如果你理解了我上面所讲的SnapHelper的原理,解决起来就很容易了。

对于滚动速度偏慢的问题,由于这个fling过程是通过SnapHelper的SmoothScroller控制的,我们在分析创建SmoothScroller对象的时候就提到SmoothScroller的calculateSpeedPerPixel()方法是在定义滚动速度的,那复写SnapHelper的createSnapScroller()方法重新定义一个SmoothScroller不就可以了么?!

//SnapHelper中该值为100,这里改为40
private static final float MILLISECONDS_PER_INCH = 40f;
@Nullable
protected LinearSmoothScroller createSnapScroller(final RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView);
final int dx = snapDistances[0];
final int dy = snapDistances[1];
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, mDecelerateInterpolator);
}
}

@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}

可以看到,代码跟SnapHelper里是一模一样的,就只是改了MILLISECONDS_PER_INCH这个数值而已,使得calculateSpeedPerPixel()返回值变小,从而让SmoothScroller的滚动速度更快。

对于一次滚动太多个Item的问题,就需要对他滚动的个数做下限制了。那在哪里对滚动的数量做限制呢?findTargetSnapPosition()方法里! 该方法的作用就是在寻找需要滚动到哪个位置的,不在这里还能在哪里?!直接看代码:

@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {

//计算一屏的item数
int deltaThreshold = layoutManager.getWidth() / getHorizontalHelper(layoutManager).getDecoratedMeasurement(currentView);

int deltaJump;
if (layoutManager.canScrollHorizontally()) {
deltaJump = estimateNextPositionDiffForFling(layoutManager,
getHorizontalHelper(layoutManager), velocityX, 0);
//对估算出来的位置偏移量进行阈值判断,最多只能滚动一屏的Item个数
if (deltaJump > deltaThreshold) {
deltaJump = deltaThreshold;
}
if (deltaJump < -deltaThreshold) {
deltaJump = -deltaThreshold;
}
if (vectorForEnd.x < 0) {
deltaJump = -hDeltaJump;
}
} else {
deltaJump = 0;
}


}

可以看到就是对估算出来的位置偏移量做下大小限制而已,就这么简单!

通过这样调整,效果已经跟Google Play基本一样了,我猜Google Play也是这样做的!看效果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最后

如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!

转发+点赞+关注,第一时间获取最新知识点

Android架构师之路很漫长,一起共勉吧!


以下墙裂推荐阅读!!!

最后祝大家生活愉快~

最后

对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

以下是今天给大家分享的一些独家干货:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

发人员,请集中精力,对基础和重要的事情做深度研究**。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

以下是今天给大家分享的一些独家干货:

[外链图片转存中…(img-Z5lzbQ8x-1715620773484)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值