android 左滑删除 一个简单的自定义控件

转自那里忘记了,并非原创


package com.guo.qlzx.nongji.service.costom;

import android.content.Context;
import android.content.res.TypedArray;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Interpolator;
import android.widget.Scroller;



/**
 * Created by 李 on 2018/5/29.
 */

public class SwipeItemLayout  extends ViewGroup {
    enum Mode{
        RESET, DRAG, FLING, CLICK
    }
    private Mode touchMode;

    private View mainItemView;

    private boolean mInLayout =false;

    private int scrollOffset;
    private int maxScrollOffset;

    private ScrollRunnable scrollRunnable;

    public SwipeItemLayout(Context context) {
        this(context,null);
    }

    public SwipeItemLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        touchMode = Mode.RESET;
        scrollOffset = 0;

        scrollRunnable = new ScrollRunnable(context);
    }

    public int getScrollOffset(){
        return scrollOffset;
    }

    public void open(){
        if(scrollOffset!=-maxScrollOffset){
            if(touchMode== Mode.FLING)
                scrollRunnable.abort();

            scrollRunnable.startScroll(scrollOffset,-maxScrollOffset);
        }
    }

    public void close(){
        if(scrollOffset!=0){
            if(touchMode== Mode.FLING)
                scrollRunnable.abort();

            scrollRunnable.startScroll(scrollOffset,0);
        }
    }

    void fling(int xVel){
        scrollRunnable.startFling(scrollOffset,xVel);
    }

    void revise(){
        if(scrollOffset<-maxScrollOffset/2)
            open();
        else
            close();
    }

    private void ensureChildren(){
        int childCount = getChildCount();

        for (int i=0;i<childCount;i++){
            View childView = getChildAt(i);
            ViewGroup.LayoutParams tempLp = childView.getLayoutParams();

            if(tempLp==null || !(tempLp instanceof LayoutParams))
                throw new IllegalStateException("缺少layout参数");

            LayoutParams lp = (LayoutParams) tempLp;
            if(lp.itemType==0x01){
                mainItemView = childView;
            }
        }

        if(mainItemView==null)
            throw new IllegalStateException("main item不能为空");
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //确定children
        ensureChildren();

        //先测量main
        LayoutParams lp = (LayoutParams) mainItemView.getLayoutParams();

        measureChildWithMargins(
                mainItemView,
                widthMeasureSpec,getPaddingLeft()+getPaddingRight(),
                heightMeasureSpec,getPaddingTop()+getPaddingBottom());

        setMeasuredDimension(
                mainItemView.getMeasuredWidth()+ getPaddingLeft()+getPaddingRight()+ lp.leftMargin+lp.rightMargin
                ,mainItemView.getMeasuredHeight()+getPaddingTop()+getPaddingBottom()+lp.topMargin+lp.bottomMargin);

        //测试menu
        int menuWidthSpec = MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED);
        int menuHeightSpec = MeasureSpec.makeMeasureSpec(mainItemView.getMeasuredHeight(),MeasureSpec.EXACTLY);
        for(int i=0;i<getChildCount();i++){
            View menuView = getChildAt(i);
            lp = (LayoutParams) menuView.getLayoutParams();

            if(lp.itemType==0x01)
                continue;

            measureChildWithMargins(menuView,menuWidthSpec,0,menuHeightSpec,0);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mInLayout = true;

        //确定children
        ensureChildren();

        int pl = getPaddingLeft();
        int pt = getPaddingTop();
        int pr = getPaddingRight();
        int pb = getPaddingBottom();

        LayoutParams lp;

        //layout main
        lp = (LayoutParams) mainItemView.getLayoutParams();
        mainItemView.layout(
                pl+lp.leftMargin,
                pt+lp.topMargin,
                getWidth()-pr-lp.rightMargin,
                getHeight()-pb-lp.bottomMargin);

        //layout menu
        int totalLength = 0;
        int menuLeft = mainItemView.getRight()+lp.rightMargin;
        for(int i=0;i<getChildCount();i++){
            View menuView = getChildAt(i);
            lp = (LayoutParams) menuView.getLayoutParams();

            if(lp.itemType==0x01)
                continue;

            int tempLeft = menuLeft+lp.leftMargin;
            int tempTop = pt+lp.topMargin;
            menuView.layout(
                    tempLeft,
                    tempTop,
                    tempLeft+menuView.getMeasuredWidth()+lp.rightMargin,
                    tempTop+menuView.getMeasuredHeight()+lp.bottomMargin);

            menuLeft = menuView.getRight()+lp.rightMargin;
            totalLength += lp.leftMargin+lp.rightMargin+menuView.getMeasuredWidth();
        }

        maxScrollOffset = totalLength;
        scrollOffset = scrollOffset<-maxScrollOffset/2 ? -maxScrollOffset:0;

        offsetChildrenLeftAndRight(scrollOffset);

        mInLayout = false;
    }

    void offsetChildrenLeftAndRight(int delta){
        for(int i=0;i<getChildCount();i++){
            View childView = getChildAt(i);
            ViewCompat.offsetLeftAndRight(childView,delta);
        }
    }

    @Override
    public void requestLayout() {
        if (!mInLayout) {
            super.requestLayout();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        removeCallbacks(scrollRunnable);
        touchMode = Mode.RESET;
        scrollOffset = 0;
    }

    //展开的情况下,拦截down event,避免触发点击main事件
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                final int x = (int) ev.getX();
                final int y = (int) ev.getY();
                View pointView = findTopChildUnder(this,x,y);
                if(pointView!=null && pointView==mainItemView && scrollOffset !=0)
                    return true;
                break;
            }

            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_CANCEL:
                break;

            case MotionEvent.ACTION_UP:{
                final int x = (int) ev.getX();
                final int y = (int) ev.getY();
                View pointView = findTopChildUnder(this,x,y);
                if(pointView!=null && pointView==mainItemView && touchMode== Mode.CLICK && scrollOffset !=0)
                    return true;
            }
        }

        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                final int x = (int) ev.getX();
                final int y = (int) ev.getY();
                View pointView = findTopChildUnder(this,x,y);
                if(pointView!=null && pointView==mainItemView && scrollOffset !=0)
                    return true;
                break;
            }

            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_CANCEL:
                break;

            case MotionEvent.ACTION_UP:{
                final int x = (int) ev.getX();
                final int y = (int) ev.getY();
                View pointView = findTopChildUnder(this,x,y);
                if(pointView!=null && pointView==mainItemView && touchMode== Mode.CLICK && scrollOffset !=0) {
                    close();
                    return true;
                }
            }
        }

        return false;
    }

    void setTouchMode(Mode mode){
        if(mode==touchMode)
            return;

        if(touchMode== Mode.FLING)
            removeCallbacks(scrollRunnable);

        touchMode = mode;
    }

    public Mode getTouchMode(){
        return touchMode;
    }

    boolean trackMotionScroll(int deltaX){
        if(deltaX==0)
            return true;

        boolean over = false;
        int newLeft = scrollOffset+deltaX;
        if((deltaX>0 && newLeft>0) || (deltaX<0 && newLeft<-maxScrollOffset)){
            over = true;
            newLeft = Math.min(newLeft,0);
            newLeft = Math.max(newLeft,-maxScrollOffset);
        }

        offsetChildrenLeftAndRight(newLeft-scrollOffset);
        scrollOffset = newLeft;
        return over;
    }

    private static final Interpolator sInterpolator = new Interpolator() {
        @Override
        public float getInterpolation(float t) {
            t -= 1.0f;
            return t * t * t * t * t + 1.0f;
        }
    };

    private class ScrollRunnable implements Runnable{
        private Scroller scroller;
        private boolean abort;
        private int minVelocity;

        ScrollRunnable(Context context){
            scroller = new Scroller(context,sInterpolator);
            abort = false;

            ViewConfiguration configuration = ViewConfiguration.get(context);
            minVelocity = configuration.getScaledMinimumFlingVelocity();
        }

        void startScroll(int startX,int endX){
            if(startX!=endX){
                Log.e("scroll - startX - endX",""+startX+" "+endX);
                setTouchMode(Mode.FLING);
                abort = false;

                scroller.startScroll(startX,0,endX-startX,0, 400);
                ViewCompat.postOnAnimation(SwipeItemLayout.this,this);
            }
        }

        void startFling(int startX,int xVel){
            Log.e("fling - startX",""+startX);

            if(xVel> minVelocity && startX!=0) {
                startScroll(startX, 0);
                return;
            }

            if(xVel<-minVelocity && startX!=-maxScrollOffset) {
                startScroll(startX, -maxScrollOffset);
                return;
            }

            startScroll(startX,startX>-maxScrollOffset/2 ? 0:-maxScrollOffset);
        }

        void abort(){
            if(!abort){
                abort = true;
                if(!scroller.isFinished()){
                    scroller.abortAnimation();
                    removeCallbacks(this);
                }
            }
        }

        @Override
        public void run() {
            Log.e("abort",Boolean.toString(abort));
            if(!abort){
                boolean more = scroller.computeScrollOffset();
                int curX = scroller.getCurrX();
                Log.e("curX",""+curX);

                boolean atEdge = false;
                if(curX!=scrollOffset)
                    atEdge = trackMotionScroll(curX-scrollOffset);

                if(more && !atEdge) {
                    ViewCompat.postOnAnimation(SwipeItemLayout.this, this);
                    return;
                }else{
                    removeCallbacks(this);
                    if(!scroller.isFinished())
                        scroller.abortAnimation();
                    setTouchMode(Mode.RESET);
                }
            }
        }
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams ? (LayoutParams) p : new LayoutParams(p);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams && super.checkLayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    public static class LayoutParams extends MarginLayoutParams{
        public int itemType = -1;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.SwipeItemLayout_Layout);
            itemType = a.getInt(R.styleable.SwipeItemLayout_Layout_layout_itemType,-1);
            a.recycle();
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }

        public LayoutParams(LayoutParams source) {
            super(source);
            itemType = source.itemType;
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }
    }

    static View findTopChildUnder(ViewGroup parent,int x, int y) {
        final int childCount = parent.getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = parent.getChildAt(i);
            if (x >= child.getLeft() && x < child.getRight()
                    && y >= child.getTop() && y < child.getBottom()) {
                return child;
            }
        }
        return null;
    }


    public static class OnSwipeItemTouchListener implements RecyclerView.OnItemTouchListener {
        private SwipeItemLayout captureItem;
        private float lastMotionX;
        private float lastMotionY;
        private VelocityTracker velocityTracker;

        private int activePointerId;

        private int touchSlop;
        private int maximumVelocity;

        private boolean parentHandled;
        private boolean probingParentProcess;

        private boolean ignoreActions = false;

        public OnSwipeItemTouchListener(Context context){
            ViewConfiguration configuration = ViewConfiguration.get(context);
            touchSlop = configuration.getScaledTouchSlop();
            maximumVelocity = configuration.getScaledMaximumFlingVelocity();
            activePointerId = -1;
            parentHandled = false;
            probingParentProcess = false;
        }

        @Override
        public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
            if(probingParentProcess)
                return false;

            boolean intercept = false;
            final int action = ev.getActionMasked();

            if(action!=MotionEvent.ACTION_DOWN && ignoreActions)
                return true;

            if(action!=MotionEvent.ACTION_DOWN && (captureItem==null||parentHandled))
                return false;

            if (velocityTracker == null) {
                velocityTracker = VelocityTracker.obtain();
            }
            velocityTracker.addMovement(ev);

            switch (action){
                case MotionEvent.ACTION_DOWN:{
                    ignoreActions = false;
                    parentHandled = false;
                    activePointerId = ev.getPointerId(0);
                    final float x = ev.getX();
                    final float y = ev.getY();
                    lastMotionX = x;
                    lastMotionY = y;

                    boolean pointOther = false;
                    SwipeItemLayout pointItem = null;
                    //首先知道ev针对的是哪个item
                    View pointView = findTopChildUnder(rv,(int)x,(int)y);
                    if(pointView==null || !(pointView instanceof SwipeItemLayout)){
                        //可能是head view或bottom view
                        pointOther = true;
                    }else
                        pointItem = (SwipeItemLayout) pointView;

                    //此时的pointOther=true,意味着点击的view为空或者点击的不是item
                    //还没有把点击的是item但是不是capture item给过滤出来
                    if(!pointOther && (captureItem ==null || captureItem !=pointItem))
                        pointOther = true;

                    //点击的是capture item
                    if(!pointOther){
                        Mode mode = captureItem.getTouchMode();

                        //如果它在fling,就转为drag
                        //需要拦截,并且requestDisallowInterceptTouchEvent
                        boolean disallowIntercept = false;
                        if(mode== Mode.FLING){
                            captureItem.setTouchMode(Mode.DRAG);
                            disallowIntercept = true;
                            intercept = true;
                        }else {//如果是expand的,就不允许parent拦截
                            captureItem.setTouchMode(Mode.CLICK);
                            if(captureItem.getScrollOffset()!=0)
                                disallowIntercept = true;
                        }

                        if(disallowIntercept){
                            final ViewParent parent = rv.getParent();
                            if (parent!= null)
                                parent.requestDisallowInterceptTouchEvent(true);
                        }
                    }else{//capture item为null或者与point item不一样
                        //直接将其close掉
                        if(captureItem !=null &&
                                captureItem.getScrollOffset()!=0) {
                            captureItem.close();
                            ignoreActions = true;
                            return true;
                        }

                        captureItem = null;

                        if(pointItem!=null) {
                            captureItem = pointItem;
                            captureItem.setTouchMode(Mode.CLICK);
                        }
                    }

                    //如果parent处于fling状态,此时,parent就会转为drag。应该将后续move都交给parent处理
                    probingParentProcess = true;
                    parentHandled = rv.onInterceptTouchEvent(ev);
                    probingParentProcess = false;
                    if(parentHandled) {
                        intercept = false;
                        //在down时,就被认定为parent的drag,所以,直接交给parent处理即可
                        if(captureItem !=null && captureItem.getScrollOffset()!=0)
                            captureItem.close();
                    }
                    break;
                }

                case MotionEvent.ACTION_POINTER_DOWN: {
                    final int actionIndex = ev.getActionIndex();
                    activePointerId = ev.getPointerId(actionIndex);

                    lastMotionX = ev.getX(actionIndex);
                    lastMotionY = ev.getY(actionIndex);
                    break;
                }

                case MotionEvent.ACTION_POINTER_UP: {
                    final int actionIndex = ev.getActionIndex();
                    final int pointerId = ev.getPointerId(actionIndex);
                    if (pointerId == activePointerId) {
                        final int newIndex = actionIndex == 0 ? 1 : 0;
                        activePointerId = ev.getPointerId(newIndex);

                        lastMotionX = ev.getX(newIndex);
                        lastMotionY = ev.getY(newIndex);
                    }
                    break;
                }

                //down时,已经将capture item定下来了。所以,后面可以安心考虑event处理
                case MotionEvent.ACTION_MOVE: {
                    final int activePointerIndex = ev.findPointerIndex(activePointerId);
                    if (activePointerIndex == -1)
                        break;

                    final int x = (int) (ev.getX(activePointerIndex)+.5f);
                    final int y = (int) ((int) ev.getY(activePointerIndex)+.5f);

                    int deltaX = (int) (x - lastMotionX);
                    int deltaY = (int)(y- lastMotionY);
                    final int xDiff = Math.abs(deltaX);
                    final int yDiff = Math.abs(deltaY);

                    Mode mode = captureItem.getTouchMode();

                    if(mode== Mode.CLICK){
                        //如果capture item是open的,下拉有两种处理方式:
                        //  1、下拉后,直接close item
                        //  2、只要是open的,就拦截所有它的消息,这样如果点击open的,就只能滑动该capture item
                        if(xDiff> touchSlop && xDiff>yDiff){
                            captureItem.setTouchMode(Mode.DRAG);
                            final ViewParent parent = rv.getParent();
                            parent.requestDisallowInterceptTouchEvent(true);

                            deltaX = deltaX>0 ? deltaX-touchSlop:deltaX+touchSlop;
                        }else/* if(yDiff>touchSlop)*/{
                            probingParentProcess = true;
                            parentHandled = rv.onInterceptTouchEvent(ev);
                            probingParentProcess = false;

                            if(parentHandled && captureItem.getScrollOffset() != 0)
                                captureItem.close();
                        }
                    }

                    mode = captureItem.getTouchMode();
                    if(mode== Mode.DRAG){
                        intercept = true;
                        lastMotionX = x;
                        lastMotionY = y;

                        //对capture item进行拖拽
                        captureItem.trackMotionScroll(deltaX);
                    }
                    break;
                }

                case MotionEvent.ACTION_UP:
                    Mode mode = captureItem.getTouchMode();
                    if(mode== Mode.DRAG){
                        final VelocityTracker velocityTracker = this.velocityTracker;
                        velocityTracker.computeCurrentVelocity(1000, maximumVelocity);
                        int xVel = (int) velocityTracker.getXVelocity(activePointerId);
                        captureItem.fling(xVel);

                        intercept = true;
                    }
                    cancel();
                    break;

                case MotionEvent.ACTION_CANCEL:
                    captureItem.revise();
                    cancel();
                    break;
            }

            return intercept;
        }

        @Override
        public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
            if(ignoreActions)
                return;

            final int action = ev.getActionMasked();
            final int actionIndex = ev.getActionIndex();

            if (velocityTracker == null) {
                velocityTracker = VelocityTracker.obtain();
            }
            velocityTracker.addMovement(ev);

            switch (action){
                case MotionEvent.ACTION_POINTER_DOWN:
                    activePointerId = ev.getPointerId(actionIndex);

                    lastMotionX = ev.getX(actionIndex);
                    lastMotionY = ev.getY(actionIndex);
                    break;

                case MotionEvent.ACTION_POINTER_UP:
                    final int pointerId = ev.getPointerId(actionIndex);
                    if(pointerId== activePointerId){
                        final int newIndex = actionIndex == 0 ? 1 : 0;
                        activePointerId = ev.getPointerId(newIndex);

                        lastMotionX = ev.getX(newIndex);
                        lastMotionY = ev.getY(newIndex);
                    }
                    break;

                //down时,已经将capture item定下来了。所以,后面可以安心考虑event处理
                case MotionEvent.ACTION_MOVE: {
                    final int activePointerIndex = ev.findPointerIndex(activePointerId);
                    if (activePointerIndex == -1)
                        break;

                    final float x = ev.getX(activePointerIndex);
                    final float y = (int) ev.getY(activePointerIndex);

                    int deltaX = (int) (x - lastMotionX);

                    if(captureItem !=null && captureItem.getTouchMode()== Mode.DRAG){
                        lastMotionX = x;
                        lastMotionY = y;

                        //对capture item进行拖拽
                        captureItem.trackMotionScroll(deltaX);
                    }
                    break;
                }

                case MotionEvent.ACTION_UP:
                    if(captureItem !=null){
                        Mode mode = captureItem.getTouchMode();
                        if(mode== Mode.DRAG){
                            final VelocityTracker velocityTracker = this.velocityTracker;
                            velocityTracker.computeCurrentVelocity(1000, maximumVelocity);
                            int xVel = (int) velocityTracker.getXVelocity(activePointerId);
                            captureItem.fling(xVel);
                        }
                    }
                    cancel();
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if(captureItem !=null)
                        captureItem.revise();

                    cancel();
                    break;

            }
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}

        void cancel(){
            parentHandled = false;
            activePointerId = -1;
            if(velocityTracker !=null){
                velocityTracker.recycle();
                velocityTracker = null;
            }
        }

    }
}

中间的其他资源文件 attrs_swipeitemlayout.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SwipeItemLayout.Layout">
        <attr name="layout_itemType">
            <flag name="main" value="0x01"/>
            <flag name="menu" value="0x02"/>
        </attr>
    </declare-styleable>
</resources>

最后就是例子= = 单个条目的写法,母为平常显示的,子为滑出来的删除

母:

 app:layout_itemType="main"

子:

 app:layout_itemType="menu"
<?xml version="1.0" encoding="utf-8"?>
<自定义地址.SwipeItemLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <LinearLayout
        android:id="@+id/ll_content"
        app:layout_itemType="main"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white">
        <TextView
            android:id="@+id/tv_name"
            android:layout_width="match_parent"
            android:layout_height="44dp"
            android:textColor="@color/textcolor3"
            android:textSize="14sp"
            android:gravity="center_vertical"
            android:lines="1"
            tools:text="123"
            android:layout_marginLeft="15dp"
            android:layout_marginRight="15dp"/>
        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="@color/gray_e5"
            android:layout_marginLeft="15dp"/>
    </LinearLayout>
    <Button
        android:id="@+id/btn_delete"
        app:layout_itemType="menu"
        android:layout_width="60dp"
        android:layout_height="44dp"
        android:text="删除"
        android:textSize="18sp"
        android:textColor="@color/white"
        android:background="@color/red_e7"/>
</自定义地址.SwipeItemLayout>

无论是recycle还是listview 都需要在加入一句话

recyclerView.addOnItemTouchListener(new SwipeItemLayout.OnSwipeItemTouchListener(this));

这样才会滑动有效,不然是无效的

自然如果已经使用这个,单个条目点击事件会失效,自需要在母布局里价格id变成点击这个id跳转即可代替。

以上转自地址忘记了= =,作者有看到可以删除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值