Android Searchable

文章来源:http://developer.android.com/guide/topics/search/search-dialog.html

一、前言:
 
Android为程序的搜索功能提供了统一的搜索接口,search dialog和search widget。
search dialog只能为于activity窗口的上方,search widget可以位于任何位置。
search dialog和search widget都会向我们的searchable activity发送消息(主要是搜索关键字)。
通过这种方式,可以为任何activity加入search dialog和search widget,系统可以启动合适的activity来处理搜索并显示结果。
search dialog和search widget的 其他属性 如下:
A声音搜索
B根据最近的搜索结果,给出搜索建议
C根据我们程序的实际搜索结果,给出搜索建议
注1search widget在 Android 3.0或更高版本才可用
注2searchable activity才是真正执行搜索的
 
二、基本知识
 
在开始实现搜索功能之前,请决定使用search dialog,还是search widget.
他们的搜索功能特性都有一样,但是他们还有微小区别。
A, search dialog是一个被系统控制的UI组件。但他被用户激活的时候,它总是出现在activity的上方,如图一所示
B, Android系统负责处理search dialog上所有的事件,当用户提交了查询,系统会把这个查询请求传输到我们的searchable activity,
让searchable activity在处理真正的查询。当用户在输入的时候,search dialog还能提供搜索建议。
C, search widget是SearchView的一个实例,你可以把它放在你的布局的任何地方
D, 默认的,search widget和一个标准的EditText widget一样,不能做任何事情。
但是你可以配置它,让android系统处理所有的按键事件,把查询请求传输给合适的activity,可以配置它让它像search dialog一样提供search suggestions。
E, search widget在 Android 3.0或更高版本才可用. search dialog没有此项限制
提示: 如果你想自己在search widget处理所有的用户输入,请使用各种回调函数和监听接口,具体参照 SearchView
图一
Searchable(上) - hubingforever - 民主与科学
当用户在search dialog或search widget中执行一个搜索的时候,系统会创建一个Intent,并把查询关键字保存在里面,
然后启动我们在AndroidManifest.xml中声明好的searchable activity,并把Intent传送给它。
实现一个可以搜索的程序,主要需要以下几个部份
(1)search dialog or widget的配置文件
配置一个XML文件用于配置search dialog 或widget的设置。对于search dialog,该配置文件的名字一般约定为 searchable.xml
(2)searchable activity
searchable activity用于接收搜索关键字,并进行数据搜索和显示搜索结果。
(3)搜索条。search dialog 或search widget
* The search dialog
默认的,search dialog是隐藏。当我们按下了SEARCH键或在程序中调用 onSearchRequested() ,它将出现在屏幕的上方.
* a SearchView widget
使用search widget的时候,你可以把该搜索条放在我们activity的任何地方。
Instead of putting it in your activity layout, however, it's usually more convenient for users as an action view in the Action Bar.
 
三、创建配置文件searchable.xml
 
配置文件说明了search dialog 或widget的一些属性。包括UI,以及suggestions 和voice search behave的一些属性。
该文件一般约定为 searchable.xml 并位于 res/xml/ 目录下。
searchable.xml 必须以 <searchable> element 作 为根节点 ,且 至少定义一个属性
比如, 示例1:
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/app_label"
android:hint="@string/search_hint" >
</searchable>
android:label 是唯一 必须定义 的属性。它指向一个字符串,它应该是 应用程序的名字
实际上该 label 也只有在search suggestions for Quick Search Box可用时才可见。
这时该label在系统设置的Searchable项的列表中可见
虽然a ndroid:hint 属性不是必须,但是还是推介总是定义它。它是search box用户输入前 输入框中的提示语
<searchable> 还有其他的一些属性。如果不需要search suggestions 和voice search的话,大多数的属性是不需要的。
关于 searchable.xml 更多内容请参考: http://developer.android.com/guide/topics/search/searchable-config.html
 

四、创建Searchable Activity


searchable activity根据搜索关键字进行搜索,并显示搜索结果。
当我们在search dialog or widget执行搜索的时候,系统就启动你的searchable activity ,并把搜索关键字用一个aciton为ACTION_SEARCH的Intent传给你的searchable activity. 你的searchable activity在Intent中通过extra的QUERY来提取搜索关键字,执行搜索并显示搜索结果.
我们需要在AndroidManifest.xml文件中声明Searchable Activity,以便在search dialog or widget执行搜索的时候,系统启动该searchable activity并把搜索关键字传给它。
4.1、声明searchable activity
如何在AndroidManifest.xml文件中声明Searchable Activity
   1. 在activity的<intent-filter> 中添加一个可以接受action为ACTION_SEARCH的intent。
   2. 在<meta-data>中指明search dialog or widget的配置文件(即searchable.xml)
   比如,示例2:
   <application ... >
    <activity android:name=".SearchableActivity" >
        <intent-filter>
            <action android:name="android.intent.action.SEARCH" />
        </intent-filter>
        <meta-data android:name="android.app.searchable"
                   android:resource="@xml/searchable"/>
    </activity>
    ...
</application>
<meta-data>必须包括android:name这个属性,而且其值必须为"android.app.searchable",
还必须包括android:resource这个属性,它指定了我们的search dialog的配置文件。(示例2中指定是the res/xml/searchable.xml).
注意: <intent-filter> 没有必要加入<category android:name="android.intent.category.DEFAULT" /> ,
因为系统是通过component name把ACTION_SEARCH intent explicitly显示的传递给searchable activity的。
4.2、执行搜索
当你在manifest文件中,声明好了searchable activity,在你的searchable activity,就可以参照下面的3步执行搜索了。
(1),提取搜索关键字
(2),在你的数据中进行搜索
(3),显示搜索结果
一般来说,你的搜索结果需要要在ListView进行显示,所有你的searchable activity需要继承ListActivity。
它包括了一个拥有单一ListView的layout,它为我们使用ListView提供了方便。
4.2.1、提取搜索关键字
当用户在search dialog or widget界面开始搜索的时候,系统将查找一个合适的searchable activity,并给它传送一个ACTION_SEARCH的intent,该intent把搜索关键字保存在extra的QUERY中。当activity的时候,我们必须检查保存在extra的QUERY中的搜索关键字,并提取它。
比如,示例3:
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.search);


    // Get the intent, verify the action and get the query
    Intent intent = getIntent();
    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
      String query = intent.getStringExtra(SearchManager.QUERY);
      doMySearch(query);
    }
}
在Action为ACTION_SEARCH的intent中,extra的QUERY总是被包含。在示例3提取处理了搜索关键字,然后让doMySearch()函数在执行真正的搜索的。
4.2.2、数据搜索
数据的存储和搜索都是和你的程序相关的。你可以以各种方式进行数据的存储和搜索,它才是你的程序更需要关注的。
但是还是有几点需要注意的:
(1),当你的数据存储在手机的数据库中时,执行full-text search (using FTS3, rather than a LIKE query)
能提供在文本数据中得到更健壮且速度更快的search。在 sqlite.org 中可以得到FTS3的更多信息, 在 SQLiteDatabase 可以看到Android上的SQLite更多信息 . 可以在 Searchable Dictionary看到一个用FTS3实现搜索的示例。
(2),如果的数据来自互联网,那么你的搜索将受制于用户的数据连接情况。这时我们就需要显示一个进度条,直到从网路返回搜索结果。
. 在 android.net 可一看到更多的network APIs ,在 Creating a Progress Dialog 可以到看到如何显示一个进度条。
如果想让的数据和搜索变得透明,那么我建议你用一个Adapter返回搜索结果。这种方式,你更容易在ListView中显示搜索结果。如果你的数据在自于数据库,那么通过CursorAdapter向ListView提供搜索结果,如果你的数据来自于其他方式,可以扩展一个BaseAdapter来使用。
4.2.3、结果显示
正如上面讨论的一样,显示搜索结果建议使用的UI是ListView,所以你的searchable activity最好继承于ListActivity,然后是使用setListAdapter()来设置和搜索结果绑定了的Adapter。这样你的搜索结果就可以显示在ListView中了。
你可以参考 Searchable Dictionary 示例,它展示了怎么进行数据库查询,怎么使用Adapter向ListView提供搜索结果。


原文来自:雨枫技术教程网 http://www.fengfly.com
原文网址:http://www.fengfly.com/plus/view-206552-1.html

五、使用Search Dialog

search dialog提供了一个上浮在屏幕上方的搜索条,应用程序的图标显示在搜索条的左边。当用户在输入的时候,它可以提供建议的搜索关键字。
当用户自行搜索的时候,系统会把它的搜索关键字searchable activity来执行真正的搜索。
但是如果你的设备使用的是Android 3.0,(或更高版本),你可以考虑使用search widget。
search dialog默认是隐藏的,直到用户激活它。如果用法的手机上有SEARCH按钮,那么按下该键,默认是激活search dialog。
为了使用search dialog,你必须想系统说明哪个searchable activity将受到该search dialog的搜索请求,以便执行搜索。比如,在示例2中,是名叫SearchableActivity的searchable activity,当然同时使用search dialog的也是它。如果你想使用其他的actvitity来显示search dialog的, 比如名字为OtherActivity, 让它来显示search dialog 并把搜索请求传递给SearchableActivity, 你必须在manifest中声明一个searchable activity
(比如SearchableActivity)来接收OtherActivity中的search dialog的搜索请求.
为一个activity的search dialog声明searchable activity,你需要在AndroidManifest.xml中代表该activity的<activity> 元素中加入<meta-data>
这个<meta-data>必须包含“android:value”属性,该属性指明了searchable activity的类名,
还必须包括属性“android:name”,且其值必须为 "android.app.default_searchable".
下面的例子就声明了两个searchable activity(SearchableActivity和OtherActivity),
OtherActivity也是在它的search dialog中使用SearchableActivity执行searches操作。
示例4
<application ... >
    <!-- this is the searchable activity; it performs searches -->
    <activity android:name=".SearchableActivity" >
        <intent-filter>
            <action android:name="android.intent.action.SEARCH" />
        </intent-filter>
        <meta-data android:name="android.app.searchable"
                   android:resource="@xml/searchable"/>
    </activity>

    <!-- this activity enables the search dialog to initiate searches
         in the SearchableActivity -->
    <activity android:name=".OtherActivity" ... >
        <!-- enable the search dialog to send searches to SearchableActivity -->
        <meta-data android:name="android.app.default_searchable"
                   android:value=".SearchableActivity" />
    </activity>
    ...
</application>
因为在OtherActivity 中已经包含了一个 <meta-data>元素,它声明了使用哪个searchable activity来执行搜索,所以它的search dialog也变得可用。按下手机的SEARCH按钮(如果有的话)或调用onSearchRequested()都将激活search dialog.
一旦用户在search dialog中执行search操作, 系统将启动SearchableActivity 并向其传送ACTION_SEARCH intent.
提示searchable activity 自己默认就提供了the search dialog ,且它的searchable activity就是自己本身,所以不需要再声明.

如果你想为你的应用程序的每个activity 都提供该search dialog, 那么请 <meta-data> 元素加入 <application> 作为其儿子, 而不是加入到每个<activity>. 通过这种方式, 每个activity 都继承了该值, 提供search dialog, 并把searches传送到同一个searchable activity. (如果你有多个searchable activities, 你可以在单个activitiy中加入不同的<meta-data>来声明searchable activity,这样就重写了默认的searchable activity )

 

六、如何启动search dialog

 

正如上面所提到的,如果当前activity声明了使用searchable activity,那么按下手机的SEARCH按钮(如果有的话)或调用onSearchRequested()都将激活search dialog.

然而,SEARCH按钮并不是所有的设备上都有,所以你需要在你的UI中提供一个搜索按钮,

以便通过调用onSearchRequested()激活search dialog 。

例如,你可以在Options Menu一个菜单项或在你的activity的布局的按钮中调用onSearchRequested()来启动search dialog.

search_icons.zip 文件中有针对medium and high density屏幕的搜索图标,你可以在你的搜索菜单项或按钮中使用它(low-density screens scale-down the hdpi image by one half).

你也可以使用"type-to-search"功能, 这样当用户在键盘进行输入的时候,将激活search dialog,并且是直接输入到search dialog.

你可以在activity的onCreate() 中调用setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL)来开启该功能

 

七、search dialog对Activity生命周期的影响

 

search dialog 只是一个浮动在屏幕上的 Dialog。它并不引起Activity栈的任何改变。 所以当search dialog 被启动的时候, 并不会有生命周期函数被调动(比如onPause())。你的activity只是失去输入焦点,因为输入焦点被转移到了search dialog.
如果你想在启动search dialog的时候被notified,那么请重写ActivityonSearchRequested()方法.
当系统调用该方法的时候,说明你的activity已经失去输入焦点,输入焦点已经转移到了search dialog, 所以你就可以针对这个事件在这里做些和你的work相关的事情(比如暂停游戏).在onSearchRequested的最后你再调用父类的onSearchRequested就可以了。
比如 示例4
@Override public boolean onSearchRequested() {
pauseSomeStuff();
return super.onSearchRequested();
}
如果用户通过按BACK键取消搜索的话,search dialog 将关闭 ,你的activity将再次获得输入焦点。你可以通过setOnDismissListener()/setOnCancelListener()注册监听器OnDismissListener/OnCancelListener来监听search dialog的关闭. 当search dialog 关闭的时候,OnDismissListener就会被调用。OnCancelListener只是在用户显式的退出search dialog时, 才被调用,当用户执行搜索的时候并不会被调用(这种情况用户只是很自然的消失,并不取消).
如果当前activity并不是我们所指定的searchable activity, 那么当用户执行搜索的时候,普通的activity生命周期事件将被触发
(它将调用onPause(),被暂停). 然而,如果当前就是current activity指定的searchable activity的话,下面的两件事情将发生:
A,默认的话, searchable activity 将调用onCreate() 来响应 该ACTION_SEARCH intent ,然后这个activity的一个新实例将被放到activity stack。这时你的searchable activity就有两个实例在activity stack 中(如果按下BACK键,将回到前一个searchable activity实例,
而不是离开searchable activity).
B ,如果你把searchable activity的 android:launchMode 属性设置为了 "singleTop", 那么searchable activity 将调用onNewIntent(Intent)来响应ACTION_SEARCH , 同时ACTION_SEARCH intent也是在这里被传入 .下面的示例5, 就是一个当searchable activity的launch mode 是 "singleTop"时,该如何处理的一个很好例子。
示例5:
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.search);
    handleIntent(getIntent());
}

@Override
protected void onNewIntent(Intent intent) {
    setIntent(intent);
    handleIntent(intent);
}

private void handleIntent(Intent intent) {
    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
      String query = intent.getStringExtra(SearchManager.QUERY);
      doMySearch(query);
    }
}

这里所有对search intent的处理都是放在handleIntent() 函数中的, 这样 在onCreate()onNewIntent() 中直接调用它就行了.

当系统调用onNewIntent(Intent)的时候,表示activity并不是新建的, 所以getIntent()返回的还是onCreate()中接受到的intent. 因此你必须在onNewIntent(Intent)调用setIntent(Intent) (这样保存的intent才被更新,之后你可以同过getIntent()来取得它).

使用"singleTop" 是常用的处理方法, 因为一旦用户执行了一次搜索,它往往还想执行一次搜索,而且创建大量的searchable activity不太好。因此建议把所有的searchable activity 都在manifest中把它设置为"singleTop" 模式 。

比如,示例6

<activity android:name=".SearchableActivity"
          android:launchMode="singleTop" >
    <intent-filter>
        <action android:name="android.intent.action.SEARCH" />
    </intent-filter>
    <meta-data android:name="android.app.searchable"
                      android:resource="@xml/searchable"/>

</activity>:

八、searchable activity传送数据

有时,你可能想在searchable activity收到的搜索关键字的基础上再添加些内容。然而有时添加的内容依取决于启动search dialog的Activity。Anroid可以让你在系统向searchable activity发送的intent时候,向该intent添加你的数据。ACTION_SEARCH intent通过携带一个名叫APP_DATABundle来携带你的数据。为了传送你的数据,请在要执行搜索请求的Acitivity中重写onSearchRequested() ,创建一个Bundle,并把你要携带的数据放在其中,然后以Bundle为参数之一来调用startSearch()激活search dialog.

比如,示例7:

@Override
public boolean onSearchRequested() {
     Bundle appData = new Bundle();
     appData.putBoolean(SearchableActivity.JARGON, true);
     startSearch(null, false, appData, false);
     return true;
 }
返回"true"表示你已经成功处理了该回调事件,调用startSearch()是为了激活search dialog. 一旦用户提交了搜索请求, 它将和你添加的数据一样被传送到searchable activity。 你可以通过APP_DATA Bundle来提取它。
比如:示例8:
Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
if (appData != null) {
    boolean jargon = appData.getBoolean(SearchableActivity.JARGON);
}
注意:不要在
onSearchRequested()之外调用
startSearch()方法。 当需要在你的activity中激活search dialog请总是onSearchRequested(). 否则, 如果onSearchRequested()没被调用,一些个性化得操作就不能得到执行(比如上面例子的添加额外数据)
 

九、使用Search Widget

Android 3.0 或更高版本中,可以使用 SearchView widget.如果你开发的程序是基于Anroid3.0且计划使用search widget, 那么我们建议把search widget 作为一个an action view in the Action Bar来使用, 而不是使用search dialog (也不要把search widget 放在你的activity layout中). 比如, 图二 就是把search widget 当做Action Bar来使用的一个界面.

search widget提供了和search dialog一样的功能. 当用户执行搜索的时候,它会启动合适的activity来进行处理,它也提供搜索关键词建议和语言搜索.

注意: 当你把search widget作为action view来使用时, 你可能有时仍然需要使用search dialog, 比如有时search widgAction Bar中也不太合适. 具体的请参照下面的”同时使用search widget和search dialog

图二

Searchable之五(使用Search Widget) - hubingforever - 民主与科学

十、配置 search widget

首先你应该向前面讲的search dialog一样,先创建好searchable配置和searchable activity,然后为每个SearchView设置好搜索助手。你可以通过setSearchableInfo()来设置你的SearchableInfo 对象,SearchableInfo它代表的是你的searchable配置。

你可以同过SearchManagergetSearchableInfo() 来得到 一个SearchableInfo的引用。

比如, 如果你想在Action Bar中把SearchView 作为一个action view来使用,那么在 回调数onCreateOptionsMenu() 中就应该enable the widget 实例10:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the options menu from XML
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.options_menu, menu);

    // Get the SearchView and set the searchable configuration
    SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
    searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
    searchView.setIconifiedByDefault(false); // Do not iconify the widget; expand it by default、
    return true;
}
这样就完了,search widget现在已经被配置好了,系统也能够把搜索命令发送到你的searchable activity. 你也可以在 search widget中使用search suggestions。
注意: 如果你想自己处理所有用户的输入,请在它的回调函数和事件监听接口方法中处理。关于SearchView 和其监听接口,更多的内容可以参照SearchView的文档。
关于Action Bar中action views的更多内容,请参照Using the Action Bar (里面有把search widget作为action view来进行添加的实例代码)
十一、search widget的其他特性
可以向SearchView 添加一些其他的特性
 
          
          
A、提交按钮(A submit button) 默认情况下是没提交搜索的按钮,所以用户必须在键盘上按下"Return"键来提交搜索.你可以同过setSubmitButtonEnabled(true)来添加一个提交按钮("submit" button) 注:这里 的"Return"键应该就是”Enter“ B、自定义search suggestions 当你使用search suggestions的时候, 你经常希望用户仅仅是简单选择suggestion, 但是他们也可能想自定义suggested search query. 你可以通过调用setQueryRefinementEnabled(true),来为每个suggestion添加一个按钮,让用户在search box中输入用户自定义的suggestion C、让 search box 可以见 默认情况下, search widget是"iconified“的,只是用一个图标 来表示它(一个放大镜), 当用户按下它的时候才显示search box . 你可以调用setIconifiedByDefault(false)让search box默认都被显示。 你也可以调用setIconified()让它以iconified“的形式显示。 SearchView 中还有其他的一些API允许你个性化search widget的显示.然而他们大多数是在你自己处理用户输入而不Android system处理输入和显示search suggestions时使用 十二、同时使用search widget和search dialog   如果你把search widget作为action view而插入到Action Bar中, 那么你可以让它只在有足够空间时才以Action Bar的形式出现(通过设置android:showAsAction="ifRoom"), 这时search widget就可能不以action view的形式出现, 而是是以菜单的形式出现在overflow menu里. 比如你的程序运行在一个小屏幕的手机上,在 Action Bar中就没有足够的空间把search widget和其他的action items or navigation elements显示下。这时它们将以菜单项的形式出现在overflow menu中. 当在overflow menu中显示的时候,该项像普通的菜单项一样 and 它不再显示 action view (the search widget). 为了处理该情况,当用户选择了和search widget关联的menu item时,你必须激活search dialog。为了处理此事情, 你必须在onOptionsItemSelected() h处理该"Search" menu item并通过调用onSearchRequested()来开启search dialog。 关于Action Bar如何工作和处理此中情况的更多信息请参照文档 Using the Action Bar也可以参照search dialog 和search widget的实例 Searchable Dictionary  

十三,语言搜索(Voice Search)

你可以通过在你的searchable配置中添加android:voiceSearchMode属性来实现search dialog或widget语言功能的添加。
这样就添加了一个用于启动voice prompt的语音搜索的按钮。一但用户完成了speaking, 这个transcribed search query将传送到你的searchable activity.
示例11如下:
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/search_label"
android:hint="@string/search_hint"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" >
</searchable>
第一值"showVoiceSearchButton"用于开启voice search, 第二个值“launchRecognizer,”说明了voice search button应该启动一个recognizer来把transcribed text传递给你的searchable activity.
你还可以使用其他的一些属性来定义voice search的行为,比如语言和返回的最大结果数。
关于 voice search的 更多属性请参照 Searchable Configuration
注意请仔细考虑是否要在你的程序中使用voice search.所有通过voice search按钮的搜索都是直接传送给了你的searchable activity,用户根本没有机会预览transcribed query。需要充分测试语言识别(voice recognition),以便它能识别用户在你的程序中提交的各种请求。

十四、Search Suggestions
 
search dialog和the search widget都可以在Android system系统的帮助下,在用户搜索的时候,提供建议搜索词。
系统将管理suggestions列表并处理用户选中suggestion事件.
一般你可以提供以下两种建议搜索词:
最近的搜索词
这些搜索词只是你以前的一些搜索关键词。更多的内容请参照 Adding Recent Query Suggestions.
个性化搜索词:
这些search suggestions都来自于你自己的数据, 它帮助用户快速选择正确的拼写或他们要搜索的项.
图三 就是来自于dictionary程序一个个性化suggestions的界面图—用户可以选择一个suggestion来很快的得到它的定义。更多内容请参照 Adding Custom Suggestions
图三:
Searchable之六(Voice Search和Search Suggestions) - hubingforever - 民主与科学
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值