02 Gallery 源码-StateManager 使用栈(先进后出)管理 ActivityState界面 与 DataManager 数据

0. 原文拜读

https://blog.csdn.net/lb377463323/article/details/69523891

1. GalleryActivity.initializeByIntent() 函数

根据不同的getIntent()意图进行初始化,我们这里主要是看桌面点击程序的初始化 initCustomerView()

package com.android.gallery3d.app;

public final class GalleryActivity extends AbstractGalleryActivity implements OnCancelListener {

    private void initializeByIntent() {
        Intent intent = getIntent();
        String action = intent.getAction();
        if (Intent.ACTION_GET_CONTENT.equalsIgnoreCase(action)) {
            mDrawerLayoutSupported = false;
            startGetContent(intent);
        } else if (Intent.ACTION_PICK.equalsIgnoreCase(action)) {
            mDrawerLayoutSupported = false;
            // We do NOT really support the PICK intent. Handle it as
            // the GET_CONTENT. However, we need to translate the type
            // in the intent here.
            Log.w(TAG, "action PICK is not supported");
            String type = Utils.ensureNotNull(intent.getType());
            if (type.startsWith("vnd.android.cursor.dir/")) {
                if (type.endsWith("/image")) intent.setType("image/*");
                if (type.endsWith("/video")) intent.setType("video/*");
            }
            startGetContent(intent);
        } else if (Intent.ACTION_VIEW.equalsIgnoreCase(action)
                || ACTION_REVIEW.equalsIgnoreCase(action)) {
            mDrawerLayoutSupported = false;
            Uri uri = intent.getData();
            if (uri != null) {
                int flag = intent.getFlags();
                int match = sURIMatcher.match(uri);
                if ((match == ALL_DOWNLOADS || match == ALL_DOWNLOADS_ID) &&
                        (flag & Intent.FLAG_GRANT_READ_URI_PERMISSION) == 0) {
                    if (checkCallingOrSelfPermission(
                            PERMISSION_ACCESS_ALL) != PackageManager.PERMISSION_GRANTED) {
                        Log.w(TAG, "no permission to view: " + uri);
                        return;
                    }
                }
            } else {
                Log.w(TAG, "uri get from intent is null");
            }
            startViewAction(intent);
        }else if(ACTION_VIEW_DESIGNATED_LIST.equalsIgnoreCase(action)) {
            /**
             * description  增加预览指定列表分支
             */
            startTimelinePage();
        }else if(ACTION_VIEW_DESIGNATED_ITEM.equalsIgnoreCase(action)){
            /**
             * description  增加预览指定Item分支
             */
            startSinglePhotoPage();
        } else {
            // 这里为默认逻辑,即接收到的action为 android.intent.action.MAIN
            // 表示为用户主动点击启动,非第三方跳转调用本程序
            
            // 屏蔽DrawerLayout
            // mDrawerLayoutSupported = true;
            mDrawerLayoutSupported = false;

            // 屏蔽进入页面时加载时间轴
            //  tartTimelinePage();
            // 增加方法入口
            initCustomerView();

            // 屏蔽Title
            //mToolbar.setTitle(R.string.albums_title);
            mToolbar.setTitle(null);
        }
        
        // 是否显示侧边菜单
        toggleNavDrawer(mDrawerLayoutSupported);
    }

这里跳转的对应关系可以在AndroidMefest.xml

        <activity android:name="com.android.gallery3d.app.GalleryActivity"
                android:theme="@style/AppTheme"
                android:configChanges="keyboardHidden|orientation|screenSize|locale|fontScale|layoutDirection|screenLayout|smallestScreenSize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" /> // 桌面启动
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.LAUNCHER" /> // 入口程序属性
                <category android:name="android.intent.category.APP_GALLERY" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.GET_CONTENT" /> // 第三方内容跳转
                <category android:name="android.intent.category.OPENABLE" />
                <data android:mimeType="vnd.android.cursor.dir/image" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.GET_CONTENT" />
                <category android:name="android.intent.category.OPENABLE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="image/*" /> // 图片
                <data android:mimeType="video/*" /> // 视频
            </intent-filter>
            <!-- We do NOT support the PICK intent, we add these intent-filter for
                 backward compatibility. Handle it as GET_CONTENT. -->
            <intent-filter>
                <action android:name="android.intent.action.PICK" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="image/*" />
                <data android:mimeType="video/*" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.PICK" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="vnd.android.cursor.dir/image" />
                <data android:mimeType="vnd.android.cursor.dir/video" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="vnd.android.cursor.dir/image" />
                <data android:mimeType="vnd.android.cursor.dir/video" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <action android:name="com.android.camera.action.REVIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="" />
                <data android:scheme="http" />
                <data android:scheme="https" />
                <data android:scheme="content" />
                <data android:scheme="file" />
                <data android:mimeType="image/*" />
                <data android:mimeType="application/vnd.google.panorama360+jpg" />
            </intent-filter>
            <intent-filter>
                <action android:name="com.android.camera.action.REVIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="http" />
                <data android:scheme="https" />
                <data android:scheme="content" />
                <data android:scheme="file" />
                <data android:mimeType="video/mpeg4" />
                <data android:mimeType="video/mp4" />
                <data android:mimeType="video/3gp" />
                <data android:mimeType="video/3gpp" />
                <data android:mimeType="video/3gpp2" />
                <data android:mimeType="application/sdp" />
            </intent-filter>

            <!-- 新增IntentFilter给文件浏览器提供多图预览列表接口 -->
            <!--<intent-filter>
                <action android:name="android.intent.action.VIEW_DESIGNATED_LIST"/>
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>-->
            <!-- 新增IntentFilter给文件浏览器提供多图预览详情接口(需要携带预览列表 用于左右滑动) -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW_DESIGNATED_ITEM"/>
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

2. 初始化自定义布局 initCustomerView(),界面切换引子

这里我们进行了自定义,将界面修改为fragment

    private List<BaseFragment> fragmentList;

    /**
     * 初始化视图 Activity拆分为多个Fragment
     * Drawer视图结构改为Tab视图结构
     */
    private void initCustomerView() {
        ...
    }
2.1 查看照片界面-TimeLinePageFragment
package com.android.gallery3d.app;

/**
 * 相册分页显示Fragment(时间轴Fragment)
 */
public class TimeLinePageFragment extends BaseFragment {
    
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Log.i(TAG, "onViewCreated");

        // 设置surfaceview控件
        setGLRoot(getView().findViewById(R.id.gl_root_view));

        ...
    }

    
    private void startTimelinePage() {
        String newBPath = getDataManager().getTopSetPath(DataManager.INCLUDE_ALL);
        String newPath = FilterUtils.switchClusterPath(newBPath, FilterUtils.CLUSTER_BY_TIME);
        Bundle data = new Bundle();
        data.putString(TimeLinePage.KEY_MEDIA_PATH, newPath);
        if (getStateManager().getStateCount() == 0)
            getStateManager().startState(TimeLinePage.class, data);
        else {
            ActivityState state = getStateManager().getTopState();
            String oldClass = state.getClass().getSimpleName();
            String newClass = TimeLinePage.class.getSimpleName();
            // if (!oldClass.equals(newClass)) {
                getStateManager().switchState(getStateManager().getTopState(),
                        TimeLinePage.class, data);
            // }
        }
    }
}

上述中使用了切换切换使用了getStateManager.startState或switchState接口, 即ActivityState页面的切换都是由StateManager来管理

    public void startDefaultPage() {
        ......
        Bundle data = new Bundle();
        data.putString(AlbumSetPage.KEY_MEDIA_PATH,
                getDataManager().getTopSetPath(DataManager.INCLUDE_ALL));
        // 显示所有相册缩略图的页面
        getStateManager().startState(AlbumSetPage.class, data);
        ......
    }
  • AlbumSetPage.java:显示所有相册缩略图的页面:public class AlbumSetPage extends ActivityState implements
  • AlbumPage.java:显示单个相册所有照片缩略图的页面:public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner,
  • PhotoPage.java:单张照:public abstract class PhotoPage extends ActivityState implements
Ps:进入Gallery显示的是AlbumSetPage;
AlbumSetPage是由多个相册组成的;
然后点击某一相册显示的AlbumPage,AlbumPage由该相册的所有缩略图组成;
再点击其中某一张图片显示的就是PhotoPage。
  • SlideshowPage.java:滑屏页面:public class SlideshowPage extends ActivityState {
  • ManageCachePage.java:缓存管理页面:public class ManageCachePage extends ActivityState implements
  • TimeLinePage.java:自定义页面:public class TimeLinePage extends ActivityState implements

基于上述,接下来界面切换我们主要进行 StateManager、DataManager、ActivityState 的关系研究

3. 基类 ActivityState

StateManager通过栈(stack)来管理要显示的界面,每个界面都是一个ActivityState类,切换界面做的就是stack的入栈和出栈操作

  • AlbumSetPage.java:显示所有相册缩略图的页面:public class AlbumSetPage extends ActivityState implements
  • AlbumPage.java:显示单个相册所有照片缩略图的页面:public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner,
  • PhotoPage.java:单张照:public abstract class PhotoPage extends ActivityState implements
  • SlideshowPage.java:滑屏页面:public class SlideshowPage extends ActivityState {
  • ManageCachePage.java:缓存管理页面:public class ManageCachePage extends ActivityState implements
  • TimeLinePage.java:自定义页面:public class TimeLinePage extends ActivityState implements
package com.android.gallery3d.app;

abstract public class ActivityState {

    // 1 实例化,在StateManager.startState() 被调用
    protected ActivityState() {
    
    }

    // 2.初始化参数,在StateManager.startState() 被调用
    void initialize(AbstractGalleryActivity activity, Bundle data) {
        mActivity = activity;
        mData = data;
    }

    
    // 3.界面显示,在StateManager.startState() 被调用
    protected void onCreate(Bundle data, Bundle storedState) {
        mBackgroundColor = GalleryUtils.intColorToFloatARGBArray(
                mActivity.getResources().getColor(getBackgroundColorId()));
    }
    
    // 4.界面显示,在StateManager.startState() 被调用
    // a subclass of ActivityState should override the method to resume itself
    protected void onResume() {
        if(null != mFragment && mFragment.isAdded()){

            RawTexture fade = mFragment.getTransitionStore().get(
                    PreparePageFadeoutTexture.KEY_FADE_TEXTURE);
            mNextTransition = mFragment.getTransitionStore().get(
                    KEY_TRANSITION_IN, StateTransitionAnimation.Transition.None);
            if (mNextTransition != StateTransitionAnimation.Transition.None) {
                mIntroAnimation = new StateTransitionAnimation(mNextTransition, fade);
                mNextTransition = StateTransitionAnimation.Transition.None;
            }
        } else {
            RawTexture fade = mActivity.getTransitionStore().get(
                    PreparePageFadeoutTexture.KEY_FADE_TEXTURE);
            mNextTransition = mActivity.getTransitionStore().get(
                    KEY_TRANSITION_IN, StateTransitionAnimation.Transition.None);
            if (mNextTransition != StateTransitionAnimation.Transition.None) {
                mIntroAnimation = new StateTransitionAnimation(mNextTransition, fade);
                mNextTransition = StateTransitionAnimation.Transition.None;
            }
        }
    }

}

上述中继承 ActivityState 需要重写onCreate() 和 onResume() 周期,例如 AlbumSetPage.java:显示所有相册缩略图的页面

package com.android.gallery3d.app;

public class AlbumSetPage extends ActivityState implements OnClickListener,
        SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner,
        EyePosition.EyePositionListener, MediaSet.SyncListener, ButtonControlsHandler.Delegate {
        
    @Override
    public void onCreate(Bundle data, Bundle restoreState) {
        super.onCreate(data, restoreState);
        // 初始化界面
        initializeViews();
        // 初始化数据
        initializeData(data);
        ...
    }
3.1 ActivityState.AlbumSetPage.onCreate.initializeViews
    private void initializeViews() {
        // mActivity -> mFragment
        if (null != mFragment && mFragment.isAdded()) {
            mSelectionManager = new SelectionManager(mFragment, true);
            mSelectionManager.setSelectionListener(this);

            mConfig = Config.AlbumSetPage.get(mActivity);

            mSlotView = new SlotView(mFragment, mConfig.slotViewSpec);
            // mActivity -> mFragment
            mAlbumSetView = new AlbumSetSlotRenderer(
                    mFragment, mSelectionManager, mSlotView,
                    mConfig.labelSpec,
                    mConfig.slotViewSpec,
                    mConfig.placeholderColor);
            mSlotView.setSlotRenderer(mAlbumSetView);
            mSlotView.setListener(new SlotView.SimpleListener() {
                @Override
                public void onDown(int index) {
                    AlbumSetPage.this.onDown(index);
                }

                @Override
                public void onUp(boolean followedByLongPress) {
                    AlbumSetPage.this.onUp(followedByLongPress);
                }

                @Override
                public void onSingleTapUp(int slotIndex) {
                    AlbumSetPage.this.onSingleTapUp(slotIndex);
                }

                @Override
                public void onLongTap(int slotIndex) {
                    AlbumSetPage.this.onLongTap(slotIndex);
                }
            });

            mActionModeHandler = new ActionModeHandler(mFragment, mSelectionManager);
            mActionModeHandler.setActionModeListener(new ActionModeListener() {
                @Override
                public boolean onActionItemClicked(MenuItem item) {
                    return onItemSelected(item);
                }
            });
            mRootPane.addComponent(mSlotView);
        } else {
            mSelectionManager = new SelectionManager(mActivity, true);
            mSelectionManager.setSelectionListener(this);

            //mConfig是用来设置SlotView的参数,而SlotView就是一个相册
            mConfig = Config.AlbumSetPage.get(mActivity);
            mSlotView = new SlotView(mActivity, mConfig.slotViewSpec);

            //mAlbumSetView是mSlotView的渲染器,控制mSlotView的显示
            mAlbumSetView = new AlbumSetSlotRenderer(
                    mActivity, mSelectionManager, mSlotView, mConfig.labelSpec,
                    mConfig.slotViewSpec,
                    mConfig.placeholderColor);

            //将mAlbumSetView设置给mSlotView        
            mSlotView.setSlotRenderer(mAlbumSetView);
            
            //监听SlotView事件,根据手势操作做出相应的响应,这个监听事件的原理后面再分析
            mSlotView.setListener(new SlotView.SimpleListener() {
                @Override
                public void onDown(int index) {
                    AlbumSetPage.this.onDown(index);
                }

                @Override
                public void onUp(boolean followedByLongPress) {
                    AlbumSetPage.this.onUp(followedByLongPress);
                }

                @Override
                public void onSingleTapUp(int slotIndex) {
                    AlbumSetPage.this.onSingleTapUp(slotIndex);
                }

                @Override
                public void onLongTap(int slotIndex) {
                    AlbumSetPage.this.onLongTap(slotIndex);
                }
            });

            mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager);
            mActionModeHandler.setActionModeListener(new ActionModeListener() {
                @Override
                public boolean onActionItemClicked(MenuItem item) {
                    return onItemSelected(item);
                }
            });
            
            //把这个SlotView作为一个子控件传给GLView,mRootPane是GLView类
            mRootPane.addComponent(mSlotView);
        }
    }

3.2 ActivityState.AlbumSetPage.onCreate.initializeData
    private void initializeData(Bundle data) {
        //获取data传入的value
        String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH);

        //获取MediaSet,前面讲了每个ActivityState页面都需要一个数据源MediaSource来获取显示数据,而MediaSource是由MediaObject组成,MediaObject相当于MediaSource的单位,MediaSet就是一个MediaObject的子类,管理一组媒体数据
        mMediaSet = getDataManager().getMediaSet(mediaPath);

        //mSelectionManager用于管理选择事件
        mSelectionManager.setSourceMediaSet(mMediaSet);

        //mAlbumSetDataAdapter类似于桥梁来连接页面和数据源
        // mActivity -> mFragment
        if (null != mFragment && mFragment.isAdded()) {
            mAlbumSetDataAdapter = new AlbumSetDataLoader(
                    mFragment, mMediaSet, DATA_CACHE_SIZE);
        } else {
            mAlbumSetDataAdapter = new AlbumSetDataLoader(
                    mActivity, mMediaSet, DATA_CACHE_SIZE);
        }
        
        mSelectionManager.setAlbumSetDataLoader(mAlbumSetDataAdapter);
        
        //设置数据加载的监听接口
        mAlbumSetDataAdapter.setLoadingListener(new MyLoadingListener());
        mAlbumSetView.setModel(mAlbumSetDataAdapter);
    }

4. DataManager

  • getDataManager().getTopSetPath(DataManager.INCLUDE_ALL)
    从单例模式的命名规则看,实例化的文件位于 DataManager.java
package com.android.gallery3d.app;

// DataManager manages all media sets and media items in the system.
//
// Each MediaSet and MediaItem has a unique 64 bits id. The most significant
// 32 bits represents its parent, and the least significant 32 bits represents
// the self id. For MediaSet the self id is is globally unique, but for
// MediaItem it's unique only relative to its parent.
//
// To make sure the id is the same when the MediaSet is re-created, a child key
// is provided to obtainSetId() to make sure the same self id will be used as
// when the parent and key are the same. A sequence of child keys is called a
// path. And it's used to identify a specific media set even if the process is
// killed and re-created, so child keys should be stable identifiers.

public class DataManager implements StitchingChangeListener {

    public static DataManager from(Context context) {
        GalleryApp app = (GalleryApp) context.getApplicationContext();
        return app.getDataManager();
    }

}

上述查看 GalleryApp 为一个接口

package com.android.gallery3d.app;

public interface GalleryApp {
    public DataManager getDataManager();
    ...
}

查看接口实现位置,即为单例模式实例化位置

package com.android.gallery3d.app;

public class GalleryAppImpl extends Application implements GalleryApp {

    @Override
    public synchronized DataManager getDataManager() {
        if (mDataManager == null) {
            mDataManager = new DataManager(this);
            mDataManager.initializeSourceMap();
        }
        return mDataManager;
    }
}

4.1 查看DataManager实例化
package com.android.gallery3d.data;

public class DataManager implements StitchingChangeListener {

    public DataManager(GalleryApp application) {
        mApplication = application;
        mDefaultMainHandler = new Handler(application.getMainLooper());
    }
4.2 查看 initializeSourceMap
package com.android.gallery3d.data;

public class DataManager implements StitchingChangeListener {

    public synchronized void initializeSourceMap() {
        if (!mSourceMap.isEmpty()) return;

        // the order matters, the UriSource must come last
        // 所有数据源
        addSource(new LocalSource(mApplication));
        addSource(new PicasaSource(mApplication));
        addSource(new ComboSource(mApplication));
        addSource(new ClusterSource(mApplication));
        addSource(new FilterSource(mApplication));
        addSource(new SecureSource(mApplication));
        addSource(new UriSource(mApplication));
        addSource(new SnailSource(mApplication));

        if (mActiveCount > 0) {
            for (MediaSource source : mSourceMap.values()) {
                source.resume();
            }
        }
    }
    

例如起码的参数实例

  • data.putString(AlbumSetPage.KEY_MEDIA_PATH, getDataManager().getTopSetPath(DataManager.INCLUDE_ALL));
    private static final String TOP_IMAGE_SET_PATH = "/combo/{/local/image,/picasa/image}";

    private static final String TOP_VIDEO_SET_PATH =
            "/combo/{/local/video,/picasa/video}";

    // This is the path for the media set seen by the user at top level.
    private static final String TOP_SET_PATH = "/combo/{/local/all,/picasa/all}";

    private static final String TOP_LOCAL_IMAGE_SET_PATH = "/local/image";

    private static final String TOP_LOCAL_VIDEO_SET_PATH = "/local/video";
    
    private static final String TOP_LOCAL_SET_PATH = "/local/all";

    public String getTopSetPath(int typeBits) {

        switch (typeBits) {
            case INCLUDE_IMAGE:
                return TOP_IMAGE_SET_PATH;
            case INCLUDE_VIDEO:
                return TOP_VIDEO_SET_PATH;
            case INCLUDE_ALL:
                return TOP_SET_PATH;
            case INCLUDE_LOCAL_IMAGE_ONLY:
                return TOP_LOCAL_IMAGE_SET_PATH;
            case INCLUDE_LOCAL_VIDEO_ONLY:
                return TOP_LOCAL_VIDEO_SET_PATH;
            case INCLUDE_LOCAL_ALL_ONLY:
                return TOP_LOCAL_SET_PATH;
            default:
                throw new IllegalArgumentException();
        }
    }

上述的 DataManager 数据作为 startState 参数传递

  • getStateManager().startState(AlbumSetPage.class, data);

5. StateManager

  • getStateManager().startState(AlbumSetPage.class, data);
    从单例模式的命名规则看,实例化的文件位于 StateManage.java
package com.android.gallery3d.app;

public class StateManager {

}
StateManager 使用栈(先进后出)管理 ActivityState界面 与 DataManager 数据
package com.android.gallery3d.app;

public class StateManager {

    private static class StateEntry {
        public Bundle data;
        public ActivityState activityState;

        public StateEntry(Bundle data, ActivityState state) {
            this.data = data;
            this.activityState = state;
        }
    }
    
}
5.1 栈 push 方法
    // 参数:ActivityState 界面 与 DataManager 相关数据
    public void startState(Class<? extends ActivityState> klass,
            Bundle data) {
        Log.v(TAG, "startState " + klass);
        ActivityState state = null;
        try {
            // newInstance()和 new() 都是用来创建新的对象
            // newInstance: 是弱类型,效率低,只能调用无参构造
            // new() : 强类型,高效率,能调用任何public构造器
            state = klass.newInstance();
        } catch (Exception e) {
            throw new AssertionError(e);
        }

        // 栈不为空
        if (!mStack.isEmpty()) {
            // 获取栈顶数据
            ActivityState top = getTopState();
            
            // 暂停栈顶
            top.transitionOnNextPause(top.getClass(), klass,
                    StateTransitionAnimation.Transition.Incoming);

            if (mIsResumed) {
                // 暂停栈顶
                top.onPause();
            }
        }

        UsageStatistics.onContentViewChanged(
                UsageStatistics.COMPONENT_GALLERY,
                klass.getSimpleName());

        // 替换参数 mActivity -> mFragment
        // 根据 ActivityState 和 DataManager 完成初始化
        if(null != mFragment && mFragment.isAdded()){
            state.initialize(mFragment, data);
        }else {
            state.initialize(mActivity, data);
        }

        // push 方法,入栈
        mStack.push(new StateEntry(data, state));

        // 界面显示
        state.onCreate(data, null);
        if (mIsResumed) state.resume();
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值