仿腾讯新闻频道定制界面效果2

转载请注明出处
http://blog.csdn.net/oddshou/article/details/73558553

上一篇文章仿腾讯新闻频道定制界面效果1中在实现过程中有很多不如意的地方,可以说是失败的。但起码是一个好的开始吧。
这两天继续将这个功能完善了一下,效果还算可以,但是还不够完美。最后我会说明不完美的地方。

先上效果图:
这里写图片描述

对了,这里我只处理了效果层面的,至于数据层应当不是什么难事了。
这里把用到的完整代码贴出来:

/**
 * Created by oddshou on 2017/6/19.
 */

public class GridLayoutAnimation extends Activity implements
        View.OnClickListener{
    private static final String TAG = "GridLayoutAnimation";
    LinearLayout rootLayout;
    static final String[] CHANNELS_CHOOSED = {"要闻", "视频", "广东", "娱乐", "体育",
            "要闻2", "视频2", "广东2", "娱乐2", "体育2",
            "要闻3", "视频3", "广东3", "娱乐3", "体育3",
            "要闻4", "视频4", "广东4", "娱乐4", "体育4",

    };

    static final String[] CHANNELS_UNCHOOSED = {"宠物", "纪录片", "文化", "动漫", "股票",
            "宠物2", "纪录片2", "文化2", "动漫2", "股票2",
            "宠物3", "纪录片3", "文化3", "动漫3", "股票3",
            "宠物4", "纪录片4", "文化4", "动漫4", "股票4",

    };
    private DragGridLayout gridLayoutUnChoosed;
    private DragGridLayout gridLayoutChoosed;
    private ArrayList<BtnData> groupDataChoosed;
    private ArrayList<BtnData> groupDataUnChoosed;
    private LayoutTransition mTransitionerTop;
    private LayoutTransition mTransitionerBottom;
    //内部调换位置
    private boolean choosedChange;
    private int choosedChangeId;
    private View moveView;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_container);
        rootLayout = (LinearLayout) findViewById(R.id.container);
        createPage();
    }

    protected void createPage() {
        //尝试过设置同一个LayoutTransition,发现会有错乱,大概是因为
        //动画是两块的,同步执行会有问题。
        mTransitionerTop = new LayoutTransition();
        mTransitionerBottom = new LayoutTransition();
        setupCustomAnimations();

        //1.titile 已选频道
        TextView chooseTitle = new TextView(this);
        chooseTitle.setWidth(200);
        chooseTitle.setHeight(100);
        chooseTitle.setText("已选频道");

        rootLayout.addView(chooseTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT));
        //2.buttons 已选频道
        gridLayoutChoosed = new DragGridLayout(this);
        gridLayoutChoosed.setChangeItemCallback(callback);
        gridLayoutChoosed.setColumnCount(4);
        gridLayoutChoosed.setLayoutTransition(mTransitionerTop);
        groupDataChoosed = new ArrayList<BtnData>();
        createBtns(CHANNELS_CHOOSED, gridLayoutChoosed, groupDataChoosed);
        rootLayout.addView(gridLayoutChoosed);
        //3.title 推荐频道
        TextView unChoosedTitle = new TextView(this);
        unChoosedTitle.setWidth(200);
        unChoosedTitle.setHeight(100);
        unChoosedTitle.setText("未选频道");

        rootLayout.addView(unChoosedTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT));

        //4.buttons 推荐频道
        gridLayoutUnChoosed = new DragGridLayout(this);
        gridLayoutUnChoosed.setColumnCount(4);
        gridLayoutUnChoosed.setLayoutTransition(mTransitionerBottom);
        groupDataUnChoosed = new ArrayList<BtnData>();
        createBtns(CHANNELS_UNCHOOSED, gridLayoutUnChoosed, groupDataUnChoosed);
        rootLayout.addView(gridLayoutUnChoosed);

    }

    private LayoutTransition.TransitionListener mTransitionListner = new LayoutTransition.TransitionListener() {
        int[] locationSrc = new int[2];

        @Override
        public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
            if (transitionType == LayoutTransition.DISAPPEARING) {
                view.getLocationInWindow(locationSrc);
            } else if (transitionType == LayoutTransition.APPEARING) {
                //点击.由上到下 终点为下面第一个。
                //由下到上,终点为上面最后一个。
                int[] locationDst = new int[2];
                if (!choosedChange) {

                    if (transition == mTransitionerTop) {
                        //由下到上
                        getLocation(locationDst);
                    } else if (transition == mTransitionerBottom) {
                        //由上到下
                        View dstView = gridLayoutUnChoosed.getChildAt(0);
                        dstView.getLocationInWindow(locationDst); //这里获取得到0,0,改用其他方式获取
                    }else {
                        return;
                    }
                }else {
                    View dstView = gridLayoutChoosed.getChildAt(choosedChangeId);
                    dstView.getLocationInWindow(locationDst); //这里获取得到0,0,改用其他方式获取

                }

                PropertyValuesHolder pvhTransX =
                        PropertyValuesHolder.ofFloat("translationX", locationSrc[0] - locationDst[0], 0f);
                PropertyValuesHolder pvhTransY =
                        PropertyValuesHolder.ofFloat("translationY", locationSrc[1] - locationDst[1], 0f);
                final ObjectAnimator animIn = ObjectAnimator.ofPropertyValuesHolder(
                        this, pvhTransX, pvhTransY).
                        setDuration(transition.getDuration(LayoutTransition.APPEARING));
                transition.setAnimator(LayoutTransition.APPEARING, animIn);
                animIn.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        choosedChange = false;
                    }
                });
            }
        }

        @Override
        public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
            container.postInvalidate(); //这主要解决转换过程中的一些异常
        }

        private void getLocation(int[] location) {
            //1.计算新增控件行列
            int childCount = gridLayoutChoosed.getChildCount();
            int columnCount = gridLayoutChoosed.getColumnCount();
            int row = (int)Math.ceil(( childCount + 1.0 )/columnCount + 0.5);
            int column = (childCount + 1) % columnCount;
            if (column > 1) {
                View columnBefor = gridLayoutChoosed.getChildAt(childCount - 1);
                columnBefor.getLocationInWindow(location);
                location[0] += columnBefor.getWidth();  //这里理论上还需要加上一些margin left,right
            }else {
                //这里认为已选频道大于1
                View columnAbove = gridLayoutChoosed.getChildAt(childCount - columnCount);
                columnAbove.getLocationInWindow(location);
                location[1] += columnAbove.getHeight(); //同样这里也是不准确的
            }

        }
    };

    private void setupCustomAnimations() {
        //把延时去掉,速度瞬间提升
//        mTransitionerTop.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
//        mTransitionerTop.setStartDelay(LayoutTransition.APPEARING, 0);
        mTransitionerTop.addTransitionListener(mTransitionListner);
        mTransitionerTop.setAnimator(LayoutTransition.DISAPPEARING, null);
        mTransitionerTop.setAnimator(LayoutTransition.APPEARING, null);
        mTransitionerTop.setInterpolator(LayoutTransition.APPEARING, new LinearInterpolator());
        mTransitionerTop.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
        mTransitionerTop.setStartDelay(LayoutTransition.APPEARING, 0);

//        mTransitionerBottom.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
//        mTransitionerBottom.setStartDelay(LayoutTransition.APPEARING, 0);
        mTransitionerBottom.addTransitionListener(mTransitionListner);
        mTransitionerBottom.setAnimator(LayoutTransition.DISAPPEARING, null);
        mTransitionerBottom.setAnimator(LayoutTransition.APPEARING, null);
        mTransitionerBottom.setInterpolator(LayoutTransition.APPEARING, new LinearInterpolator());
        mTransitionerBottom.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
        mTransitionerBottom.setStartDelay(LayoutTransition.APPEARING, 0);

    }

    protected void createBtns(String[] titles, ViewGroup rootView, ArrayList<BtnData> groupList) {
        for (int i = 0; i < titles.length; i++) {
            Button btn = new Button(this);
//            btn.setOnLongClickListener(this);
            btn.setOnClickListener(this);
//            btn.setOnDragListener(this);
            btn.setWidth(160);
            btn.setText(titles[i]);
            btn.setBackgroundResource(R.drawable.button_selector);
            btn.setTextColor(Color.BLACK);
            BtnData btnData = new BtnData(i, titles[i], titles == CHANNELS_CHOOSED);

            btn.setTag(btnData);
            groupList.add(btnData);
            GridLayout.LayoutParams lp = new GridLayout.LayoutParams();
            lp.setMargins(10, 10, 10, 10);
            btn.setLayoutParams(lp);
            rootView.addView(btn);
        }
    }

    @Override
    public void onClick(View v) {
        //已选频道点击移动到未选频道
        //未选频道点击移动到已选频道
        BtnData tag = (BtnData) v.getTag();
        if (tag.choosed) {
            gridLayoutChoosed.removeView(v);
            //这里如果不添加新的btn 会有问题,原因似乎是原btn有一些坐标属性
            //导致添加到新的父控件计算位置有误
            gridLayoutUnChoosed.addView(v, 0);

            tag.choosed = false;
            if (groupDataChoosed.contains(tag)) {
                groupDataChoosed.remove(tag);
            }
            if (!groupDataUnChoosed.contains(tag)) {
                groupDataUnChoosed.add(tag);
            }
        } else {
            gridLayoutUnChoosed.removeView(v);
            gridLayoutChoosed.addView(v);
            tag.choosed = true;
            if (!groupDataChoosed.contains(tag)) {
                groupDataChoosed.add(tag);
            }
            if (groupDataUnChoosed.contains(tag)) {
                groupDataUnChoosed.remove(tag);
            }
        }
    }


    public class BtnData {
        int id;
        String title;
        boolean choosed;

        public BtnData(int id, String title, boolean choosed) {
            this.id = id;
            this.title = title;
            this.choosed = choosed;
        }
    }

    private DragGridLayout.ChangeItemCallback callback = new DragGridLayout.ChangeItemCallback() {

        @Override
        public void changeItem(int indexEnd, View childView) {
            if (choosedChange)
                return;
            int indexStart = gridLayoutChoosed.indexOfChild(childView);
            choosedChange = true;
            choosedChangeId =  indexEnd > indexStart ? indexEnd-1 : indexEnd;
            //如果起点小于终点,计算终点坐标时减1,大于终点不减
//            View view = gridLayoutChoosed.getChildAt(indexStart);
            gridLayoutChoosed.removeView(childView);
            gridLayoutChoosed.addView(childView, indexEnd);
//            Logger.i(TAG, "changeItem: " + indexStart + " : " + indexEnd, "oddshou");
        }
    };


}

自定义gradLayout

public class DragGridLayout extends GridLayout {

    private static final int scrollSpeed = 20;
    private static final String TAG = "DragGridLayout";
    /**
     * 长按视为拖动图标
     */
    private long dragRespondTime = 500;

    /**
     * 手指按下的点坐标
     */
    private int mDownX, mDownY;

    /**
     * 手指移动的距离
     */
    private int moveX, moveY;

    /**
     * 状态栏高度
     */
    private int mStatusHeight = 0;

    /**
     * 手指按下坐标到屏幕边框的偏移值
     */
    private int mOffsetTop, mOffsetLeft;

    /**
     * 手指按下的position
     */
    private int mDragPosition = 0;

    /**
     * 手指按下的view
     */
    private View mStartDragItemView;

    /**
     * 镜像imageview组件
     */
    private ImageView mDragImageView;

    WindowManager.LayoutParams winLayoutParams;

    /**
     * 是否支持拖动界面
     */
    private boolean isDrag = false;

    /**
     * 震动
     */
    private Vibrator vibrator;

    private WindowManager windowManager;
    private static final long HOVER_TIEM = 200;
    private static final int LONG_PRESS = 2;
    private static final int HOVER =3;
    private boolean changing;
    private Handler handler = new Handler(){

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case LONG_PRESS:
                    // 绘图缓存
                    mStartDragItemView.setDrawingCacheEnabled(true);
                    // 创建镜像
                    Bitmap mDragBitmap = Bitmap.createBitmap(mStartDragItemView
                            .getDrawingCache());
                    // 释放缓存
                    mStartDragItemView.destroyDrawingCache();
                    isDrag = true;
                    createDragImage(mDragBitmap, mDownX, mDownY);
                    mStartDragItemView.setBackgroundResource(R.drawable.btn_bg_dashgap);
                    break;
                case HOVER:
                    if ( !changing) {
//                        Logger.i(TAG, "dispatchTouchEvent: " + moveX + " : " + moveY, "oddshou");
                        int index = pointToPosition(moveX, moveY);
                        int indexStart = indexOfChild(mStartDragItemView);
                        if (index != -1 && index != indexStart && mCallback != null) {
                            Logger.i(TAG, "dispatchTouchEvent: " + indexStart + " : " + index, "oddshou");
                            changing = true;
                            mCallback.changeItem(index, mStartDragItemView);
                            mDragPosition = index;
                        }
                        changing = false;
                    }
                    break;
            }
        }
    };

    public DragGridLayout(Context context) {
        super(context);
        windowManager = (WindowManager) context
                .getSystemService(Context.WINDOW_SERVICE);
        mStatusHeight = getStatusHeight(context);
    }

    public void createAnimation() {
        PropertyValuesHolder pvhTransX =
                PropertyValuesHolder.ofFloat("translationX",  -mStartDragItemView.getWidth()/2 + moveX - mStartDragItemView.getLeft(),  0);
        PropertyValuesHolder pvhTransY =
                PropertyValuesHolder.ofFloat("translationY", -mStartDragItemView.getHeight() +15 + moveY - mStartDragItemView.getTop(),  0);
        final ObjectAnimator animIn = ObjectAnimator.ofPropertyValuesHolder(
                mStartDragItemView, pvhTransX, pvhTransY).
                setDuration(500);
        animIn.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {

            }
        });
        animIn.start();


    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:

                mDownX = (int) ev.getX();
                mDownY = (int) ev.getY();
                mOffsetLeft = (int) ev.getRawX() - mDownX;
                mOffsetTop = (int) ev.getRawY() - mDownY;

                Log.i(TAG, "手指按下ev.getX---------->" + mDownX + "  ev.getY-------->"
                        + mDownY);

                mDragPosition = pointToPosition(mDownX, mDownY);

                if (mDragPosition == -1) {
                    return super.dispatchTouchEvent(ev);
                }
                mStartDragItemView = getChildAt(mDragPosition);

                if (mStartDragItemView == null) {
                    return super.dispatchTouchEvent(ev);
                }
                // 500秒长按认为是拖动事件
                handler.removeMessages(LONG_PRESS);
                handler.sendEmptyMessageDelayed(LONG_PRESS, dragRespondTime);

                break;
            case MotionEvent.ACTION_MOVE:
                if (isDrag && mDragImageView != null) {
                    int newMoveX = (int) ev.getX();
                    int newmoveY = (int) ev.getY();
                    updateDragImage(newMoveX, newmoveY);
                    if (Math.abs(moveX - newMoveX) < 10 && Math.abs(moveY - newmoveY) < 10) {
                        //认为没有移动
//                        moveX = newMoveX;
//                        moveY = newmoveY;
                    }else {
                        moveX = newMoveX;
                        moveY = newmoveY;
                        handler.removeMessages(HOVER);
                        handler.sendEmptyMessageDelayed(HOVER, HOVER_TIEM);
                    }

                }
                break;
            case MotionEvent.ACTION_UP:
                handler.removeMessages(LONG_PRESS);
                handler.removeMessages(HOVER);
                moveX = (int) ev.getX();
                moveY = (int) ev.getY();
                if (isDrag && mDragImageView != null) {
//                handler.removeCallbacks(scrollRunnable);
                    isDrag = false;
                    mStartDragItemView.setBackgroundResource(R.drawable.button_selector);
                    windowManager.removeView(mDragImageView);
                    createAnimation();
                }
                break;

            default:
                break;
        }
        return super.dispatchTouchEvent(ev);

    }

    /**
     * 拖动时添加镜像
     *
     * @param mDragBitmap
     * @param mDownX
     * @param mDownY
     */
    private void createDragImage(Bitmap mDragBitmap, int mDownX, int mDownY) {
        // TODO Auto-generated method stub
        winLayoutParams = new WindowManager.LayoutParams();
        winLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
        Log.i(TAG, "createDragImage ev.getX---------->" + mDownX
                + "  ev.getY-------->" + mDownY);
        winLayoutParams.x = -mStartDragItemView.getWidth()/2 + mDownX + mOffsetLeft;
        winLayoutParams.y = -mStartDragItemView.getHeight() + 15 + mDownY + mOffsetTop - mStatusHeight;
        winLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        winLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        winLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
        mDragImageView = new ImageView(getContext());
        mDragImageView.setImageBitmap(mDragBitmap);
        // 添加到wm管理中
        windowManager.addView(mDragImageView, winLayoutParams);

    }

    /**
     * 拖动item界面更新
     */
    private void updateDragImage(int moveX, int moveY) {
        winLayoutParams.x = -mStartDragItemView.getWidth()/2 + moveX + mOffsetLeft;
        winLayoutParams.y = -mStartDragItemView.getHeight() +15 + moveY + mOffsetTop - mStatusHeight;
        windowManager.updateViewLayout(mDragImageView, winLayoutParams);

    }

    /**
     * Rectangle used for hit testing children
     */
    private Rect mTouchFrame;

    /**
     * 动画过程中,这个方法不准确
     * @param x
     * @param y
     * @return
     */
    public int pointToPosition(int x, int y) {
        if (changing) {
            return -1;
        }
        Rect frame = mTouchFrame;
        if (frame == null) {
            mTouchFrame = new Rect();
            frame = mTouchFrame;
        }

        final int count = getChildCount();
        for (int i = count - 1; i >= 0; i--) {
            final View child = getChildAt(i);

            if (child.getVisibility() == View.VISIBLE && child.getAnimation() == null) {
                child.getHitRect(frame);
                if (frame.contains(x, y)) {
                    return i;
                }
            }
        }
        return -1;
    }

    /**
     * 获取状态栏高度
     *
     * @param context
     * @return
     */
    public static int getStatusHeight(Context context) {
        int statusHeight = 0;
        Rect rect = new Rect();
        ((Activity) context).getWindow().getDecorView()
                .getWindowVisibleDisplayFrame(rect);
        statusHeight = rect.top;
        if (statusHeight == 0) {
            Class<?> localClass;
            try {
                localClass = Class.forName("com.android.internal.R$dimen");
                Object localObject = localClass.newInstance();
                int i5 = Integer.parseInt(localClass
                        .getField("status_bar_height").get(localObject)
                        .toString());
                statusHeight = context.getResources().getDimensionPixelSize(i5);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return statusHeight;
    }

    public interface ChangeItemCallback{
        void changeItem(int indexEnd, View childView);
    }

    private ChangeItemCallback mCallback;
    public void setChangeItemCallback(ChangeItemCallback callback) {
        this.mCallback = callback;
    }

}

这里硬贴两个类确实很冗余,我的demo是处理在一个项目中的子页面所以就不直接上传完成项目了。
这里开始是重点讲解了:
重点内容
1、 前一篇博客中已经将点击交换给做了,所以这里就不太谈论这部分,但是这里对之前的效果进行了优化。
2、这一篇主要处理点击拖拽,拖拽控件交换位置,松手回弹。
那么本文参考了这篇文章:
http://blog.csdn.net/adfsadsfa/article/details/50630470
用window 实现拖拽的控件,这个思路还是可以的。
这里大致以两个事件展开-长按和悬停。长按识别创建拖拽的动画控件,悬停处理控件交换。
事件处理在dispatchTouchEvent中监听
细节这里似乎没有什么特别难的地方以下随便挑几处说道说道:

    public void createAnimation() {
        PropertyValuesHolder pvhTransX =
                PropertyValuesHolder.ofFloat("translationX",  -mStartDragItemView.getWidth()/2 + moveX - mStartDragItemView.getLeft(),  0);
        PropertyValuesHolder pvhTransY =
                PropertyValuesHolder.ofFloat("translationY", -mStartDragItemView.getHeight() +15 + moveY - mStartDragItemView.getTop(),  0);
        final ObjectAnimator animIn = ObjectAnimator.ofPropertyValuesHolder(
                mStartDragItemView, pvhTransX, pvhTransY).
                setDuration(500);
        animIn.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {

            }
        });
        animIn.start();


    }

这个方法用来处理拖拽结束后,回弹的动画,发现translation 这个东西非常好用,主要是目标点填0就好了。
-mStartDragItemView.getWidth()/2 + moveX 这里不是直接用的 moveX 是因为做了一个简单的偏移效果,腾讯新闻中也是有的。这个很有意义。y轴同

    private Handler handler = new Handler(){

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case LONG_PRESS:
                    // 绘图缓存
                    mStartDragItemView.setDrawingCacheEnabled(true);
                    // 创建镜像
                    Bitmap mDragBitmap = Bitmap.createBitmap(mStartDragItemView
                            .getDrawingCache());
                    // 释放缓存
                    mStartDragItemView.destroyDrawingCache();
                    isDrag = true;
                    createDragImage(mDragBitmap, mDownX, mDownY);
                    mStartDragItemView.setBackgroundResource(R.drawable.btn_bg_dashgap);
                    break;
                case HOVER:
                    if ( !changing) {
//                        Logger.i(TAG, "dispatchTouchEvent: " + moveX + " : " + moveY, "oddshou");
                        int index = pointToPosition(moveX, moveY);
                        int indexStart = indexOfChild(mStartDragItemView);
                        if (index != -1 && index != indexStart && mCallback != null) {
                            Logger.i(TAG, "dispatchTouchEvent: " + indexStart + " : " + index, "oddshou");
                            changing = true;
                            mCallback.changeItem(index, mStartDragItemView);
                            mDragPosition = index;
                        }
                        changing = false;
                    }
                    break;
            }
        }
    };

长按500ms判定,悬停200ms
这里使用悬停之前,用过直接在dispatch 的move中判断,但是效果非常不理想,改用悬停判断效果还可以。
这里再说一下bug,未完美的地方:
1、拖拽结束回弹的时候原控件(虚线框)会消失,因为它跑去做动画了。
2、translation 动画会被其他控件遮挡,这主要是 view zorder的问题,我这里也得不到很好的解决。
以上两个问题,若仁兄有解决方案还请告知。留言或者email。oddshou@sina.com

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值