第一行代码Android 第四章(碎片Fragment,动态加载布局,碎片实践:一个简易版的新闻应用)

本文深入探讨Android中的碎片Fragment,说明其在平板应用中的重要性。介绍了Fragment的创建、动态添加、返回栈管理和生命周期,并分享了如何通过限定符实现动态加载布局,以适应不同设备。最后,通过构建一个简易新闻应用展示了Fragment的最佳实践。

第四章:手机平板要兼顾--探究碎片
4.1 碎片是什么
        碎片(Fragment)是一种可以嵌入在活动当中的UI片段,它能让程序更加合理和充分的利用大屏幕的空间,因而在平板上应用的非常广泛。
        它和活动很像,同样都能包含布局,同样都有自己的生命周期。
        平板上:新闻标题界面和新闻详细内容界面分别放在两个碎片中,然后在同一个活动中引入这两个碎片。
4.2 碎片的使用方式
        碎片通常都是在平板上使用的,首先应该创建一个平板模拟器。
4.2.1 碎片的简单用法

//在一个活动中添加两个碎片 
//左侧碎片布局 left_fragment.xml
<LinearLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Button"
        />
</LinearLayout>
//这个布局,只放置了一个按钮,并让她水平居中显示。
//新建右侧碎片布局
//right_fragment.xml
<LinearLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:background="00ff00"  //绿色
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="20sp"
        android:text="This is right fragment"
        />
</LinearLayout>

注意:Fragment有两种
        1)android.app.Fragment 系统内置的
        2)android.support.v4.app.Fragment   support-v4库中的  (建议使用)(这里不需要添加依赖就可以使用,因为build.gradle已经添加了appcompat-v7库的依赖,这个库中包含support-v4) 

//新建一个LeftFragment类,并让他继承自Fragment
public class LeftFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.left_fragment,container,false);
        return view;
    }
}
//同样再新建一个RightFragment
public class RightFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.right_fragment,container,false);
        return view;
    }
}

注意:可以通过android:name来制定碎片的类名,一定要把包名也加上 

//接下来修改activity_main中的代码(活动)
<LinearLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <fragment
        android:id="@+id/left_fragment"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    <fragment
        android:id="@+id/right_fragment"
        android:name="com.example.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
</LinearLayout>

4.2.2 动态添加碎片
        碎片的真正强大之处在于它可以在程序运行时动态的添加到活动中

//在以上代码中继续完善,新建another_right_fragment.xml
<LinearLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:background="ffff00"  
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="20sp"
        android:text="This is another right fragment"
        />
</LinearLayout>
//新建一个AnotherRightFragment类作为另一个右侧碎片
public class LeftFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.another_right_fragment,container,false);
        return view;
    }
}
//接下来修改activity_main中的代码(活动)
<LinearLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <fragment
        android:id="@+id/left_fragment"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    //将右侧碎片替换成了一个FragmentLayout中,这个空间默认都会摆放在布局的左上角,不需要任何定位
    <FragmentLayout
        android:id="@+id/right_layout"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    </FragmentLayout>
</LinearLayout>
//下面需要在代码中对FrgmentLayout添加内容,从而实现动态添加碎片的功能,修改MainActivity中的代码
public class MainActivity extends AppCompatActivity implements View.onClickListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);
        replaceFragment(new RightFrgment());  //使用replaceFragment方法动态添加RightFragment碎片
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button:
                //当点击左侧按钮时,又会把右侧的碎片替换成AnotherRightFragment碎片
                replaceFragment(new AnotherRightFragment()); 
                break;
            default:
                break;
        }
    }
    private void replaceFragment(Fragment fragment) {
        //通过getSupportFragmentManager方法获取FragmentManager
        FragmentManager fragmentManager = getSupportFragmentManager();
        //通过beginTransction方法开启一个事务
        FragmemyTransaction transaction = fragmentManager.beginTransaction();
        //向容器中添加或替换碎片,一般使用replace方法实现,需要传入容器的id和待添加的碎片实例
        transaction.replace(R.id.right_layout,fragment);
        //commit方法提交事务
        transaction.commit();
    }
}

4.2.3 在碎片中模拟一个返回栈
        只需要在事务提交前调用addToBackStack()方法就行,用于将一个事务添加到返回栈中

//下面需要在代码中对FrgmentLayout添加内容,从而实现动态添加碎片的功能,修改MainActivity中的代码
public class MainActivity extends AppCompatActivity implements View.onClickListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);
        replaceFragment(new RightFrgment());  //使用replaceFragment方法动态添加RightFragment碎片
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button:
                //当点击左侧按钮时,又会把右侧的碎片替换成AnotherRightFragment碎片
                replaceFragment(new AnotherRightFragment()); 
                break;
            default:
                break;
        }
    }
    private void replaceFragment(Fragment fragment) {
        //通过getSupportFragmentManager方法获取FragmentManager
        FragmentManager fragmentManager = getSupportFragmentManager();
        //通过beginTransction方法开启一个事务
        FragmemyTransaction transaction = fragmentManager.beginTransaction();
        //向容器中添加或替换碎片,一般使用replace方法实现,需要传入容器的id和待添加的碎片实例
        transaction.replace(R.id.right_layout,fragment);
        //将一个事务添加到返回栈中,参数一般填写null即可
        transaction.addToBackStack(null);
        //commit方法提交事务
        transaction.commit();
    }
}

4.2.3 碎片和活动之间进行通信
        虽然碎片都是嵌入在活动中现实的,可是实际上他们的关系并没有那么亲密,碎片和活动都是各自存在一个独立的类中的。
        为了方便碎片和活动之间进行通信,FragmentManager提供了一个类似于findViewById()的方法,专门用于从布局文件中获取碎片的。

RightFragment rightFragment = (RightFragment) getSupportFragmentManager().findFragementById(R.id.right_fragment);

        还可以通过getActivity方法来得到与当前碎片相关联的活动实例

MainActivity activity = (MainActivity) getActivity();
//有了活动实例后,在碎片里调用活动的方法就变得轻而易举了
//当碎片总需要使用Context对象时,也可以使用getActivity方法 ,因为活动本身就是一个Context

注意:既然碎片和活动之间可以通信,其实碎片和碎片之间也是可以通信的,可以在一个碎片中得到与他相关的活动,在通过这个活动去获取另一个碎片的实例,就可以实现碎片之间的通信了。

4.3 碎片的生命周期
4.3.1碎片的状态的回调
        1)运行状态:碎片所关联的活动时运行状态,碎片也是。
        2)暂停状态:由于另一个未占满屏幕的活动被添加到了栈顶(对话框),与他相关的碎片就会进入到暂停状态。
        3)停止状态:当一个活动进去停止状态时,碎片也是;或者FragmentTransction的remove方法和replace方法将碎片从活动中移除,但如果在事务提交前调用了addToBackStack方法,这时碎片也会进入到停止状态。停止状态的碎片对用户来说完全不可见,有可能会被系统回收。
        4)销毁状态:活动销毁,碎片销毁。或者FragmentTransction的remove方法和replace方法将碎片从活动中移除,但如果在事务提交前没有调用了addToBackStack方法,这时也会进入到销毁状态。

onArrach():碎片和活动认识
onCreate():初始化活动
onCreateView():为碎片创建布局
onActivityCreated():活动已经创建完毕
onStart():活动由不可见变为可见
onResume():活动准备和用户进行交互
碎片已被激活(用户点击返回键或碎片被移除)(当碎片被添加到返回栈,然后被移除/替换)
onPause():系统准备去启动或恢复另一个活动时调用的
onStop():活动完全不可见
onDestroyView():碎片关联的视图被移除
onDestory():活动被销毁之前
onDetach():碎片和活动解除关系

4.4 动态加载布局的技巧
        解决程序能够根据设备的分辨率或屏幕大小在运行时来决定加载哪个布局问题
4.4.1 使用限定符
        借助限定符(Qualifiers)来实现运行时判断程序应该使用双页模式还是单页模式

//接下来修改activity_main中的代码(活动)
//这里只留下一个左侧碎片,并让他充满整个父布局
<LinearLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <fragment
        android:id="@+id/left_fragment"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
//在res目录下新建layout-large文件夹,在这个文件夹下新建一个activity_main.xml
<LinearLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <fragment
        android:id="@+id/left_fragment"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    <fragment
        android:id="@+id/right_layout"
        android:name="com.example.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3" />
</LinearLayout>

注意:上面这两个,第一个布局是单页,第二个布局是双页,通过限定费用large来实现,那些屏幕被认为是large的设备会自动加载layout-large文件夹下的布局,小屏幕设备会加载layout文件夹下的布局。
        然后将MainActivity中replaceFragment方法里的代码注释掉。因为他会自动调用。

Android常见限定符参考标准

屏幕特征 限定符 描述
大小small 提供给小屏幕的资源
normal 提供给中等屏幕的资源
large 提供给大屏幕的资源
xlarge 提供给超大屏幕的资源
分辨率ldpi 提供给低分辨设备的资源(120dpi以下)
mdpi 提供给中等分辨设备的资源(120dpi-160dpi)
hpdi 提供给高分辨设备的资源(160dpi-240dpi)
xhdpi 提供给超高分辨设备的资源(240dpi-320dpi)
xxhdpi 提供给超超高分辨设备的资源(320dpi-480dpi)
方向land 提供给横屏设备的资源
port 提供给竖屏设备的资源

4.4.2 使用最小宽度限定符
 large限定符成功解决了单页双页的问题,但是large到底多大,可以使用最小宽度限定符(Smallest-widthQualifier)来实现

//在res目录下新建layout-sw600dp文件夹,在这个文件夹下新建一个activity_main.xml
//这个代表当程序运行在屏幕宽度大于等于600pd的设备上是,会加载layout-sw600dp下的activity_main.xml
//比600pd小就加载layout/activity_main布局
<LinearLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <fragment
        android:id="@+id/left_fragment"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    <fragment
        android:id="@+id/right_layout"
        android:name="com.example.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3" />
</LinearLayout>

4.5 碎片的最佳实践
        编写一个简易版的新闻应用,要求可以同时兼容手机和平板

//由于会使用到RecyclerView,因此需要首先添加依赖app/bulid.gradle
compile 'com.android.support:recyclerview-v7:24.2.1'
//准备一个新闻实体
public class News {
    private String title;
    private String content;
    public String getTitle(){
        return title
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content
    }
}
//新建一个布局文件news_content_frag.xml
<RelativeLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <LinearLayout
            android:id="@+id/visibility_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:visibility="invisible" /> //该布局暂时不可见
        <TextView  //新闻头部
            android:id="@+id/news_title"
            android:layout_width="match_[arent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="10dp"
            android:textSize="20sp" />
        <View  //中间用小细线分隔
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000" />
        <TextView  //新闻正文
            android:id="@+id/news_content"
            android:layout_width="match_[arent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:padding="15dp"
            android:textSize="18sp" />
    </LinearLayout>
</RelativeLayout>
//新建一个NewsContentFragment类
public class NewsContentFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.news_content_fragment,container,false);
        return view;
    }
    //refresh方法是用于将新闻的标题和内容显示在界面上的
    public void refresh(String newsTitle,String newsContent) {
        View visibilityLayout = view.findViewById(R.id.visibility_layout);
        visibilityLayout.setVisibility(View.VISIBLE);
        TextView newsTitleText = (TextView) view.findViewById(R.id.news_title);
        TextView newsContentText = (TextView) view.findViewById(R.id.news_content);
        newsTitleText.setText(newsTitle);  //刷新新闻的标题
        newsContenttext.setText(newsContent);  //刷新新闻的内容
    }
}
//创建一个活动,新建一个NewsContentActivity文件,,并将布局名指定成news_content,然后修改news-content.xml中的代码
<LinearLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <fragment
        android:id="@+id/news_content_fragment"
        android:name="com.example.fragmenttest.NewsContentFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
//修改NewsContentAcivity中的代码
public class NewsContentActvity extends AppCompatActivity {
    //actionStart方法完成了Intent的构造,并把活动所需要的数据也传递了进来
    public static void actionStart(Context,String newTitle,String newsContent){
        Intent intent = new Intent(context,NewsComtentActivity.class);
        intent.putExtra("news_title",newsTitle);
        intent.putExtra("news_content",newsContent);
        context.startActivity(intent);  //启动活动
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.news_content);
        String newsTitle = getIntent().getStringExtra("news_title"); //获取新闻标题
        String newsContent = getIntent().getstringExtra("news_content"); //获取新闻正文
        NewsContentFragment newsContentFragment = (NewsContentFragment) getSupportFragmentManager() findFragmentById(R.id.new_content_fragment);
        newsContentFragment.refresh(newsTitle,newsContent); //刷新NewsContent-Fragment 界面
    }
}
//创建一个用于显示新闻列表的布局,新建news_title_frag.xml
//只有一个显示新闻列表的RecyclerView
<LinearLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <android.support.v7.widget.RecyclerView
        android:id="@+id/news_title_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
//新建news_item.xml作为RecyclerView子项的布局
<TextView xmlns:android = "http://schemas.android.com/apk/res/android"
    android:id="@+id/new_title"
    android:layou_width="match_parent"
    android:layout_height="wrap_content"
    android:maxLines="1" //规定最多只能一行
    android:ellipsize="end" //规定超出一行的缩短后面的
    android:textSize="18sp"
    android:paddingLeft="10dp"  //控件边缘留白,不让内容紧紧靠在边缘上
    android:paddingRight="10dp"
    android:paddingTop="15dp"
    android:paddingBottom="15dp" />
//新闻列表和子项布局都创建好了,接下来需要一个展示新闻列表的地方
//新建NewsTitleFragment作为展示新闻列表的碎片
public class newsTitleFragment extend Fragment {
    private boolean isTwoPane;
    @Override
    public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.news_title_frag,container,false);
        return view;
    }
    @Override
    public void onActivityCreated(Bundle sacedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (getActivity().findViewById(R.id.news_content_layout) != null) {
            isTwoPane = true; //可以找到news——content_layout布局时为双页模式
        } else {
            isTwoPane = false; //否则 单页
        }
    }
}
//修改activity_main.xml中的代码来实现  这个代表单页模式加载的碎片
<FragmeLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/news_title_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <fragment
        android:id="@+id/news_title_fragment"
        android:name="com.example.fragmenttest.NewsTitleFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FragmeLayout>
//layout-sw600dp文件夹下的activity_main.xml
<LinearLayout xmln:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <fragment
        android:id="@+id/news_title_fragment"
        android:name="com.example.fragmenttest.NewsTitleFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent" 
        andorid:layout_weight="1" />
    <FragmentLayout
        android:id="@+id/news_content_layout"
        android:layout_width="0dp"
        android:layout_height="match_parent" 
        andorid:layout_weight="3" >
        <fragment
            android:id="@+id/news_content_fragment"
            android:name="com.example.fragmenttest.NewsContentFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"  />
    </FragmentLayout>
            
</LinearLayout>
//在NewsTitle-Fragment中通过RecyclerView将新闻列表展示出来
//新建一个内部类NewsAdapter来作为RecyclerView的适配器
public class NewsTitleFragment extends Fragment {
    private boolean isTwoPane;
    ...
    class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
        private List<news> mNewsList;
        class ViewHolder extends RecyclerView.ViewHolder {
            TextView newsTitleText;
            public ViewHolder(View view) {
                super(view);
                newsTitleText = (TextView) view.findViewById(R.id.news_title);
            }
        }
        public NewsAdapter(List<News> newList) {
           mNewsList =newList;
        }
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.new_item,parent,false);
            final ViewHolder holder = new ViewHolder(view);
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    News news = mNewsList.get(holder.getAdapterPosition());
                    if(isTwoPane) {
                        //如果是双页,则刷新NewsContentFragment
                        //双页则更新新闻内容碎片里的数据
                        NewsContentFragment newsContentFragment = (NewsContentFragment) getFragmentManager().findFragmentById(R.id.news_content_fragment);
                        newsContentFragment.refesh(news.getTitle(),news.getContent());
                    } else  {
                        //如果是单页模式,则直接启动NewsContentActivity
                        //启动新活动显示新闻内容
                        NewsContentActivity.actionStart(getActivity(),news.getTitle(),news.getContent());
                    }
                }
            });
            return holder;
        }
        @Override
        public void onBindViewHolder(ViewHolder holder,int position) {
            News news = mNewsList.get(position);
            holder.newsTitleText.setText(news.getTitle());
        }
        @OVerride
        public int getItemCount() {
            return mNewsList.size();
        }
    }
}
//最后一步,向RecyclerView中填充数据,修改NewsTitle_Fragment中的代码
public class NewsTitleFragment extends Fragment {
    ...
    @Override
    publlic View onCreateView(LayoutInfalter inflater,ViewGroup container,Bundle savedInstanceState){
        View view = inflater.inflate(R.layout.news_title_frag,container,false);        
        RecyclerView newsTitleRecyclerView = (RecyclerView) view.findViewById(R.id.news_title_recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
        newsAdapter adapter = new NewsAdapter(getNews());
        newsTitleRecyclerView.setAdapter(adapter);
        return view;
    }
    private List<News> getNews() {
        List<News> newsList = new ArrayList<>();
        for(int i = 1;i <= 50; i++) {
            News news = new News();
            news.setTitle("This is news title" + i );
            news.setContent(getRandomLengthContent("This is news Content" + i +"."));
            newsList.add(news);
            return newList;
        }
    }
    private String getRandomLengthContent(String content) {
        Random random = new Random();
        int length = random.nextInt(20)+1;
        StringBuilder builder = new StringBuilder(); //StringBuilder是一个可变的字符序列
        for(int i = 0;i < length; i++) {
            builder.append(content);
        }  
        return builder.toString();
    }
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值