android emojicon游戏 modern con...

3.9 TabSpec与TabHost
TabHost类官方文档地址:
Android&实现tab视图有2种方法,一种是在布局页面中定义&tabhost&标签,另一种就是继承tabactivity.但是我比较喜欢第二种方式,应为如果页面比较复杂的话你的XML文件会写得比较庞大,用第二种方式XML页面相对要简洁得多。
&?xml version=&1.0& encoding=&utf-8&?&
&LinearLayout xmlns:android=&/apk/res/android&
android:layout_width=&match_parent&
android:layout_height=&match_parent&
android:background=&@drawable/mm1&
android:orientation=&vertical& &
android:id=&@+id/btn&
android:layout_width=&match_parent&
android:layout_height=&wrap_content&
android:text=&第一个Tab& /&
android:id=&@+id/et&
android:layout_width=&match_parent&
android:layout_height=&wrap_content&
android:hint=&第二个Tab& /&
&LinearLayout
android:id=&@+id/myLayout&
android:layout_width=&match_parent&
android:layout_height=&match_parent&
android:background=&@drawable/mm2&
android:orientation=&vertical& &
android:layout_width=&match_parent&
android:layout_height=&wrap_content&
android:text=&第三个Tab& /&
android:layout_width=&match_parent&
android:layout_height=&wrap_content&
android:hint=&第三个Tab& /&
&/LinearLayout&
&/LinearLayout&
import android.app.TabA
import android.os.B
import android.view.LayoutI
import android.widget.TabH
import android.widget.TabHost.OnTabChangeL
import android.widget.TabHost.TabS
import android.widget.T
public class MainActivity extends TabActivity implements OnTabChangeListener {
private TabSpec ts1, ts2, ts3;// 声明3个分页
private TabHost tabH// 分页菜单(tab容器)
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
tabHost = this.getTabHost();// 实例(分页)菜单
// 利用LayoutInflater将布局与分页菜单一起显示
LayoutInflater.from(this).inflate(R.layout.activity_main,
tabHost.getTabContentView());
ts1 = tabHost.newTabSpec(&tabOne&);// 实例化一个分页
ts1.setIndicator(&分页1&);// 设置此分页显示的标题
ts1.setContent(R.id.btn);// 设置此分页的资源Id
ts2 = tabHost.newTabSpec(&tabTwo&);
// 设置此分页显示的标题和图标
ts2.setIndicator(&分页2&,
getResources().getDrawable(R.drawable.ic_launcher));
ts2.setContent(R.id.et);
ts3 = tabHost.newTabSpec(&tabThree&);
ts3.setIndicator(&分页3&);
ts3.setContent(R.id.myLayout);// 设置此分页的布局ID
tabHost.addTab(ts1);// 菜单中添加ts1分页
tabHost.addTab(ts2);
tabHost.addTab(ts3);
tabHost.setOnTabChangedListener(this);
public void onTabChanged(String tabId) {
//这里的tabId对应的是实例中每个分页传入的分页ID,而不是TabSpec.setIndicator()设置的标题
if (tabId.equals(&tabOne&)) {
Toast.makeText(this, &分页1&, Toast.LENGTH_SHORT).show();
if (tabId.equals(&tabTwo&)) {
Toast.makeText(this, &分页2&, Toast.LENGTH_SHORT).show();
if (tabId.equals(&tabThree&)) {
Toast.makeText(this, &分页3&, Toast.LENGTH_SHORT).show();
上面这个Activity继承了TabActivity
TabActivity的现状
官方文档在介绍TabActivity有下面这么一句话
大概的意思是说:这个类已经在Android4.0的系统中被弃用了,新的应用程序应该使用Fragment来代替该类的开发
TabActivity是否还有存在的必要性
其实谷歌有此举动,我们也应该早就想到了,为什么会这么说呢?那就要从TabActivity的原理开始说起了。
做个假定先: 比如我们最外面的Activity是MainActivity, 第一个tab是FirstActivty, 第二个tab是SecondActivity。
相信大家都用过TabActivity, 它是一个特殊的Activity,它特殊的地方在哪里?有以下几点为证:&
&1& 它看起来违反了Activity的单一窗口的原则。因为它可以同时加载几个activity, 当用户点击它上面的tab时,就会跳到相应的Activity上面去。
&2& 用户首先进去FirstActivity,然后进去SecondActivity,再点击返回键的时候。它返回的界面不是FirstActivity,而是退出我们的应用程序。
&3& 当用户在FirstActivity按返回键的时候,如果MainActivity和FirstActivity通过重写onKeyDown()方法,那么收到事件回调的只有FirstActivity。
谷歌当时的困扰
&1& 首先我们要明白一点,android系统是单窗口系统,不像windows是多窗口的(比如在windows系统上,我们可以一边聊QQ,一边斗地主等等)。也就是说,在一个时刻,android里面只有一个activity可以显示给用户。这样就大大降低了操作系统设计的复杂性(包括事件派发等等)。
&2& 但是像TabActivity那种效果又非常必要,用户体验也比较好。所以我觉得当时google开发人员肯定很纠结,于是,一个畸形的想法产生了,就是在单窗口系统下加载多个activity,它就是TabActivity。
TabActivity实现加载多个Activity原理
我们都知道,想启动一个Activity,一般是调用startActivty(Intent i)方法,然后这个方法会辗转调用到ams(ActivityManagerService)来启动目标activity,所以,TabActivity实现的要点有两个:
&1& 找到一个入口,这个入口可以访问到ActivityThread类(这个类是隐藏的,应用程序是访问不到的),然后调用ActivityThread里面的启动activity方法
&2& 绕开ams,就是我们TabActivity加载的FirstActivity和SecondActivity是不能让ams知道的。
所以,一个新的类诞生了 ---- LocalActivityManager , 它的作用如下:
&1& 这个类和ActivityThread处于一个包内,所以它有访问ActivityThread的权限。
&2& 这个类提供了类似Ams管理Activity的方法,比如调用activity的onCreate方法,onResume()等等,维护了activity生命周期。
也正如其名字一样,它是本地的activity管理。就是说它运行的进程和它管理的Activity是在一个进程里面。所以,当TabActivity要启动一个activity的时候,会调用到LocalActivityManager的创建activity方法,然后调用ActivityThread.startActivityNow(),这个方法绕过了ams,就是说ams此时根本不知道LocalActivityManager已经在暗渡陈仓的启动了一个activity(所以ams的task列表里面没有新启动activity的记录,所以用户按back键就直接退出我们的应用)。然后和正常启动activity一样,初始化activity,在初始化activity的时候,有个方法非常重要:activity.attch()
final void attach(...){
mWindow.setCallback(this);
&mWindow.setCallback(this)这个方法非常重要,它设置了window的回调接口,这是我们activity能够接受到key事件的关键所在!因为在DecorView在接受到事件的时候,会回调这个接口,如:
final Callback cb = getCallback();
final boolean handled = cb != null && mFeatureId & 0 ? cb.dispatchKeyEvent(event) : super.dispatchKeyEvent(event);
当我们启动FirstActivity的时候,我们设置FirstActivity为PhoneWindow的回调实现,所以,按back键的时候,调用的是FirstActivity的onKeyDown方法。
TabActivity小结
从以上的种种分析来看,TabActivity只是一个怪胎而已。所以,在后面的发展中肯定会被代替,只是没想到会被替代的这么快。不经让我有了一种英雄暮路,美人辞暮的感觉,至少TabActivity曾经在Android2.2/2.3版本那么显赫一时,不过终究还是逃不过被谷歌遗弃的命运。
TabActivity实现方法
说了这么多,那就让我们来看看它当年到底是怎样的叱咤风云,我们将使用两种不同的方式来实现,但是最终的效果都是一样的,
如下图所示:
具体的编码实现
(1)第一种实现方式:自定义TabWidget
1、首先创建一个TabWidget的布局文件,main_tab_layout1.xml:
&?xml version=&1.0& encoding=&utf-8&?&
&TabHost xmlns:android=&/apk/res/android&
android:id=&@android:id/tabhost&
android:layout_width=&fill_parent&
android:layout_height=&fill_parent&&
&LinearLayout
android:orientation=&vertical&
android:layout_width=&fill_parent&
android:layout_height=&fill_parent&&
&FrameLayout
android:id=&@android:id/tabcontent&
android:layout_width=&fill_parent&
android:layout_height=&0.0dip&
android:layout_weight=&1.0& /&
&TabWidget
android:id=&@android:id/tabs&
android:layout_width=&fill_parent&
android:layout_height=&wrap_content&
android:padding=&2dip&
android:background=&@drawable/tab_widget_background&
android:layout_weight=&0.0&/&
&/LinearLayout&
&/TabHost&
& & &&1&&不管你是使用TabActivity 还是自定义TabHost,都要求以TabHost作为XML布局文件的根;
& & &&2& 将FrameLayout的属性值layout_weight设置为了1.0,这样就可以把TabWidget的组件从顶部挤了下来变成了底部菜单栏。
& & &&3& &TabWidger& 和&FrameLayout&的Id 必须使用系统id,分别为android:id/tabs 和 android:id/tabcontent 。因为系统会使用者两个id来初始化TabHost的两个实例变量(mTabWidget 和 mTabContent)。
2、然后在定义一个tab_item_view.xml布局文件,这个布局文件在后面初始化Tab按钮的时候会用到&
&?xml version=&1.0& encoding=&utf-8&?&
&LinearLayout xmlns:android=&/apk/res/android&
android:layout_width=&wrap_content&
android:layout_height=&wrap_content&
android:gravity=¢er&
android:orientation=&vertical& &
&ImageView
android:id=&@+id/imageview&
android:layout_width=&wrap_content&
android:layout_height=&wrap_content&
android:focusable=&false&
android:padding=&3dp& &
&/ImageView&
android:id=&@+id/textview&
style=&@style/tab_item_text_style&
android:layout_width=&wrap_content&
android:layout_height=&wrap_content& &
&/TextView&
&/LinearLayout&
3、这里我为了方便Tab按钮字体和背景格式的统一,在styles.xml数据文件中还添加了以下内容:
&style name=&tab_item_text_style&&
&item name=&android:textSize&&10.0dip&/item&
&item name=&android:textColor&&#ffffff&/item&
&item name=&android:ellipsize&&marquee&/item&
&item name=&android:singleLine&&true&/item&
&style name=&tab_item_background&&
&item name=&android:textAppearance&&@style/tab_item_text_style&/item&
&item name=&android:gravity&¢er_horizontal&/item&
&item name=&android:background&&@drawable/selector_tab_background2&/item&
&item name=&android:layout_width&&fill_parent&/item&
&item name=&android:layout_height&&wrap_content&/item&
&item name=&android:button&&@null&/item&
&item name=&android:drawablePadding&&3.0dip&/item&
&item name=&android:layout_weight&&1.0&/item&
4、定义一个自定义Tab按钮资源文件,selector_tab_background.xml:
&?xml version=&1.0& encoding=&utf-8&?&
&selector xmlns:android=&/apk/res/android&&
&item android:drawable=&@drawable/tab_item_p& android:state_pressed=&true&/&
&item android:drawable=&@drawable/tab_item_d& android:state_selected=&true&/&
&/selector&
5、最后在定义几个用来存放Tab选项卡内容的activity布局文件,由于几个布局文件的内容都差不多,所以这里就列出一个给读者参考,有需要的话可以直接下载,layout_activity1.xml:
&?xml version=&1.0& encoding=&utf-8&?&
&LinearLayout xmlns:android=&/apk/res/android&
android:layout_width=&fill_parent&
android:layout_height=&fill_parent&&
&ImageView
android:id=&@+id/imageview&
android:layout_width=&fill_parent&
android:layout_height=&fill_parent&
android:scaleType=&fitCenter&
android:src=&@drawable/mm1& &
&/ImageView&
&/LinearLayout&
6、布局完毕,接下来讲解java代码,定义一个常量工具类,Constant.java:
Constant.java
7、定义自定义Tab选项卡Activity类,在这个类中我们可以采用两种方法编写标签页:
&1& 第一种是继承TabActivity ,然后使用getTabHost()获取TabHost对象;
&2& 第二种方法是使用自定的TabHost在布局文件上&TabHost&的自定义其ID,然后通过findViewById(),方法获得TabHost对象。&
本文中采用是继承TabActivity的方法,TabActivity1.java:
TabActivity1.java
这段代码比较复杂,我们需要详细分析一下:
& &1& 首先需要做的是获取TabHost对象,可以通过TabActivtiy里的getTabHsot()方法;
& &2& 接着向TabHost添加tabs.即调用tabHost.addTab(TabSpec) 方法。TabSpec主要包含了setIndicator 和 setContent 方法,通过这两个方法来指定Tab 和 TanContent;
& &3& TabSpec 通过&.newTabSpec(String tag)来创建实例。实例化后对其属性进行设置。setIndicator()设置tab,它有3个重载的函数:
public TabHost.TabSpec& setIndicatior(CharSwquence label,Drawable icon).指定tab的标题和图标。public TabHost.TabSpec (View view)通过View来自定义tabpublic TabHost.TabSpec(CharSequence label) 指定tab的标题,此时无图标。
& &4&&setContent 指定tab的展示内容,它也有3种重载:
public TabHost.TabSpec setContent(TabHost.TabContentFactory )public TabHost.TabSpec setContent(int ViewId)public TabHost.TabSpec setContent(Intent intent)  
& & &后两种方法比较后理解一个是通过 ViewId指定显示的内容,如.setContent(R.id.Team_EditText),第三种则是直接通过Intent加载一个新的Activity页。如.setContent(new Intent(this, MeetingActivity.class)));
8、最后再定义Tab选项卡内容的Activity,显示对应的布局页面就行了,这里只列出一个,Activity1.java:
package com.example.
import android.app.A
import android.os.B
public class Activity1 extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_activity1);
(二)第二中实现方式:隐藏TabWidget,通过RadioGroup和RadioButton实现底部菜单栏
这种方式更漂亮,也更灵活,大部分的应用程序基本都是使用这种方式,通过setCurrentTabByTag()方法来切换不同的选项卡。&
1、首先创建一个布局界面,main_tab_layout2.xml:
&?xml version=&1.0& encoding=&utf-8&?&
&TabHost xmlns:android=&/apk/res/android&
android:id=&@android:id/tabhost&
android:layout_width=&fill_parent&
android:layout_height=&fill_parent& &
&LinearLayout
android:layout_width=&fill_parent&
android:layout_height=&fill_parent&
android:orientation=&vertical& &
&FrameLayout
android:id=&@android:id/tabcontent&
android:layout_width=&fill_parent&
android:layout_height=&0.0dip&
android:layout_weight=&1.0& /&
&TabWidget
android:id=&@android:id/tabs&
android:layout_width=&fill_parent&
android:layout_height=&wrap_content&
android:layout_weight=&0.0&
android:visibility=&gone& /&
&RadioGroup
android:id=&@+id/main_radiogroup&
android:layout_width=&fill_parent&
android:layout_height=&wrap_content&
android:layout_gravity=&bottom&
android:background=&@drawable/tab_widget_background&
android:gravity=¢er_vertical&
android:orientation=&horizontal&
android:padding=&2dip& &
&RadioButton
android:id=&@+id/RadioButton0&
style=&@style/tab_item_background&
android:drawableTop=&@drawable/tab_icon1&
android:text=&主页&
android:textColor=&#ffffff&/&
&RadioButton
android:id=&@+id/RadioButton1&
style=&@style/tab_item_background&
android:drawableTop=&@drawable/tab_icon2&
android:text=&关于&
android:textColor=&#ffffff&/&
&RadioButton
android:id=&@+id/RadioButton2&
style=&@style/tab_item_background&
android:drawableTop=&@drawable/tab_icon3&
android:text=&设置&
android:textColor=&#ffffff&/&
&RadioButton
android:id=&@+id/RadioButton3&
style=&@style/tab_item_background&
android:drawableTop=&@drawable/tab_icon4&
android:text=&搜索&
android:textColor=&#ffffff&/&
&RadioButton
android:id=&@+id/RadioButton4&
style=&@style/tab_item_background&
android:drawableTop=&@drawable/tab_icon5&
android:text=&更多&
android:textColor=&#ffffff&/&
&/RadioGroup&
&/LinearLayout&
&/TabHost&
2、然后在定义几个用来存放Tab选项卡内容的activity布局文件,同上activity1_layout.xml。
3、最后再定义一个自定义Tab按钮的资源文件,selector_tab_background2.xml:
&?xml version=&1.0& encoding=&utf-8&?&
&selector xmlns:android=&/apk/res/android&&
&item android:drawable=&@drawable/tab_item_p& android:state_pressed=&true&/&
&item android:drawable=&@drawable/tab_item_d& android:state_checked=&true&/&
&/selector&
4、布局界面讲解完毕,接下来详细讲解java代码
package com.example.
import android.app.TabA
import android.content.I
import android.os.B
import android.widget.RadioB
import android.widget.RadioG
import android.widget.RadioGroup.OnCheckedChangeL
import android.widget.TabH
import android.widget.TabHost.TabS
import com.example.hiyou.Constant.ConV
功能描述:第二种实现方式,自定义RadioGroup
public class TabActivity2 extends TabActivity {
//定义TabHost对象
private TabHost tabH
//定义RadioGroup对象
private RadioGroup radioG
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_tab_layout2);
initView();
initData();
* 初始化组件
private void initView(){
//实例化TabHost,得到TabHost对象
tabHost = getTabHost();
//得到Activity的个数
int count = ConValue.mTabClassArray.
for(int i = 0; i & i++){
//为每一个Tab按钮设置图标、文字和内容
TabSpec tabSpec = tabHost.newTabSpec(ConValue.mTextviewArray[i]).setIndicator(ConValue.mTextviewArray[i]).setContent(getTabItemIntent(i));
//将Tab按钮添加进Tab选项卡中
tabHost.addTab(tabSpec);
//实例化RadioGroup
radioGroup = (RadioGroup) findViewById(R.id.main_radiogroup);
* 初始化组件
private void initData() {
// 给radioGroup设置***事件
radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(RadioGroup group, int checkedId) {
switch (checkedId) {
case R.id.RadioButton0:
tabHost.setCurrentTabByTag(ConValue.mTextviewArray[0]);
case R.id.RadioButton1:
tabHost.setCurrentTabByTag(ConValue.mTextviewArray[1]);
case R.id.RadioButton2:
tabHost.setCurrentTabByTag(ConValue.mTextviewArray[2]);
case R.id.RadioButton3:
tabHost.setCurrentTabByTag(ConValue.mTextviewArray[3]);
case R.id.RadioButton4:
tabHost.setCurrentTabByTag(ConValue.mTextviewArray[4]);
((RadioButton) radioGroup.getChildAt(0)).toggle();
* 给Tab选项卡设置内容(每个内容都是一个Activity)
private Intent getTabItemIntent(int index){
Intent intent = new Intent(this, ConValue.mTabClassArray[index]);
5、最后再定义Tab选项卡内容的Activity,同上Activity1.java。
源代码下载:
资料来源:
3.10 ListView
&ListView类官方文档地址:
ListView(列表视图)是一个常用的组件,ListView里面的每个子项Item可以是一个字符串,也可以是一个组合控件。其数据内容以列表形式直接展示出来,比如做一个游戏的排行榜,对话列表等等都可以使用列表来实现,且ListView的优点是列表中的数据可以自适应屏幕大小。
在android中,由于数据来源多种多样,如从资源文件读取、从数据库中读取、从网络上其他地方读取,而最终这些数据都将被展示在ListView中,所以android就用adapter设计模式,对应每种数据来源使用对应的adapter来连接数据和视图。Adapter就是数据和视图之间的桥梁,数据在adapter中做处理,然后显示到ListView上面。
下面主要介绍三种adapter:ArrayAdapter&T&、SimpleAdapter和SimpleCursorAdapter。
1.ArrayAdapter&T&:最简单的适配器
ArrayAdapter类官方文档地址:
首先创建存放ListView的Activity所需要的布局activity_main.xml文件。
&LinearLayout xmlns:android=&/apk/res/android&
xmlns:tools=&/tools&
android:layout_width=&match_parent&
android:layout_height=&match_parent&
tools:context=&.MainActivity& &
android:id=&@+id/list&
android:layout_width=&match_parent&
android:layout_height=&match_parent& /&
&/LinearLayout&
上面代码创建了一个布局配置文件,里面只放了一个ListView控件,将其ID设置为:list。
接下来是list_item.xml,用来设置ListView中每个Item的布局,是ListItem的XML实现。
Android提供了多种ListItem的Layout (R.layout),以下是较为常用的:
android.R.layout.simple_list_item_1
//一行text
android.R.layout.simple_list_item_2
//一行title,一行text
android.R.layout.simple_list_item_single_choice
//单选按钮
android.R.layout.simple_list_item_multiple_choice
//多选按钮
android.R.layout.simple_list_item_checked
//checkbox
我们可以自定义自己的Layout(list_item.xml):
&TextView xmlns:android=&/apk/res/android&
android:layout_width=&match_parent&
android:layout_height=&match_parent&
android:textStyle=&bold&
android:textSize=&30sp&
android:padding=&10sp&&
&/TextView&
要注意的是自定义list_item.xml的根节点必须是TextView,否则就会有ArrayAdapter requires the resource ID to be a TextView的错误。
最后是MainActivity.java代码,先找出ListView,然后往ListView里填充数组data。
package com.example.
import android.app.A
import android.os.B
import android.view.V
import android.widget.AdapterV
import android.widget.AdapterView.OnItemClickL
import android.widget.ArrayA
import android.widget.ListV
import android.widget.T
public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
* 初始化组件
private void initView() {
String[] data = { &列表1&, &列表2&, &列表3&, &列表4&, &列表5& };
// 绑定XML中的ListView,作为data的容器
ListView listview = (ListView) findViewById(R.id.list);
* 实例化适配器
* 第一个参数:Context
* 第二个参数:ListView中每一行布局样式
* 第三个参数:列表数据容器
ArrayAdapter&String& arrayAdapter = new ArrayAdapter&String&(this,
R.layout.list_item, data);
listview.setAdapter(arrayAdapter);// 将适配器数据映射ListView上
listview.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView&?& arg0, View arg1, int arg2,
long arg3) {
Toast.makeText(MainActivity.this,
&当前选中列表项的下标为:& + arg2, Toast.LENGTH_SHORT).show();
显示一个带有数据的ListView的步骤如下:
1.实例一个添加数据的容器,并将数据放入容器。
2.实例列表适配器,并且实例适配器时将数据传入。
3.实例一个ListView,并且为其设置适配器。
4.利用setContentView()函数显示ListView
因为列表中每一项数据都是一个Item,所以将ListView绑定使用OnItemClickListener项单击***器,并且重写***器中的onItemClick()函数。
onItemClick()函数的第一个参数是出发的适配器,第二个参数数触发的视图,第三个参数是适配器中项的位置下标,第四个参数是ListView项下标。
2.SimpleAdapter:具有很好扩展性的适配器,可以显示自定义内容。
SimpleAdapter类官方文档地址:
修改前面Demo的list_item.xml和MainActivity.class文件
&?xml version=&1.0& encoding=&utf-8&?&
&LinearLayout xmlns:android=&/apk/res/android&
android:layout_width=&match_parent&
android:layout_height=&match_parent&
android:orientation=&horizontal& &
&ImageView
android:id=&@+id/iv&
android:layout_width=&80dp&
android:layout_height=&80dp&
android:layout_marginLeft=&8dp&
android:layout_marginRight=&8dp& /&
&LinearLayout
android:layout_width=&wrap_content&
android:layout_height=&wrap_content&
android:orientation=&vertical& &
android:id=&@+id/bigtv&
android:layout_width=&wrap_content&
android:layout_height=&wrap_content&
android:layout_marginTop=&3dp&
android:textSize=&20sp& /&
android:id=&@+id/smalltv&
android:layout_width=&wrap_content&
android:layout_height=&wrap_content&
android:textSize=&12sp& /&
&/LinearLayout&
&/LinearLayout&
package com.example.
import java.util.ArrayL
import java.util.HashM
import java.util.L
import android.app.A
import android.os.B
import android.view.V
import android.widget.AdapterV
import android.widget.AdapterView.OnItemClickL
import android.widget.ListV
import android.widget.SimpleA
import android.widget.T
public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
* 初始化组件
private void initView() {
// 创建动态数组数据源
List&HashMap&String, Object&& data = new ArrayList&HashMap&String, Object&&();
// 实例化一个列表数据容器
HashMap&String, Object& map1 = new HashMap&String, Object&();
// 往列表容器中添加数据
* map.put(String key,Object value) 第一个参数用于初始化适配器时需要映射数据对应的索引;
* 第二个参数表示对应自定义项布局中的组件数据
* 进行添加数据时,每一个put()函数都对应自定义ListView项中的一个组件;按钮、复选框等组件是无法映射的。
map1.put(&item1_imageview&, R.drawable.list1);
map1.put(&item1_bigtv&, &一加手机发布:强调手感 &);
map1.put(&item1_smalltv&, &国内手机新品牌一加手机今日在北京发布其首款产品,这是一款强调设计的手机新品,配备骁龙801处理器。16GB版售价1999.99元。&);
// 将列表数据添加到列表容器中
data.add(map1);
HashMap&String, Object& map2 = new HashMap&String, Object&();
map2.put(&item1_imageview&, R.drawable.list2);
map2.put(&item1_bigtv&, & LG L90美国发售&);
map2.put(&item1_smalltv&, &今日,LG L90正式在美国以T-Mobile定制机的形式进行发售,售价为228美元。&);
data.add(map2);
// 绑定XML中的ListView,作为data的容器
ListView listview = (ListView) findViewById(R.id.list);
// 动态数组数据源中与ListItem中每个显示项对应的Key
String[] from = new String[] {
&item1_imageview&, &item1_bigtv&, &item1_smalltv&};
// ListItem的XML文件里面的一个ImageView ID和两个TextView ID
int[] to = new int[] {
R.id.iv, R.id.bigtv, R.id.smalltv
// 将动态数组数据源data中的数据填充到ListItem的XML文件list_item.xml中去
// 从动态数组数据源data中,取出from数组中key对应的value值,填充到to数组中对应ID的控件中去
* 实例化SimpleAdapter适配器构造函数Simple(Contect context,List data,int resource,String[] from,int[] to)
context:当前context对象
* data:ListView各项数据
* resource:ListView每一项的布局
* from:每一项布局中的数据映射索引数组
* to:每一项中数据对应的组件ID数组
SimpleAdapter adapter = new SimpleAdapter(this, data, R.layout.list_item, from, to);
listview.setAdapter(adapter);// 将适配器数据映射ListView上
listview.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView&?& arg0, View arg1, int arg2,
long arg3) {
Toast.makeText(MainActivity.this,
&当前选中列表项的为第& + (arg2+1)+&列。&, Toast.LENGTH_SHORT).show();
3.SimpleCursorAdapter
SimpleCursorAdapter类官方文档地址:
&下面用SimpleCursorAdapter来实现上一节中用SimpleAdapter实现的同样的效果,activity_main.xml文件和list_item.xml文件都不需要更改,只需要更改MainActivity.java代码。
package com.example.
import android.app.A
import android.content.ContentV
import android.database.C
import android.os.B
import android.view.V
import android.widget.AdapterV
import android.widget.AdapterView.OnItemClickL
import android.widget.ListV
import android.widget.SimpleCursorA
import android.widget.T
public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
* 初始化组件
private void initView() {
DBHelper dbHelper = new DBHelper(this);
// 向数据库中插入数据
insertDataIntoDB(dbHelper);
Cursor cursor = dbHelper.query();
// 绑定XML中的ListView,作为data的容器
ListView listview = (ListView) findViewById(R.id.list);
// 动态数组数据源中与ListItem中每个显示项对应的Key,要与创建的数据库列名一样
String[] from = new String[] {
&iv&, &bigtv&, &smalltv&};
// ListItem的XML文件里面的一个ImageView ID和两个TextView ID
int[] to = new int[] {
R.id.iv, R.id.bigtv, R.id.smalltv
// 将动态数组数据源data中的数据填充到ListItem的XML文件list_item.xml中去
// 从动态数组数据源data中,取出from数组中key对应的value值,填充到to数组中对应ID的控件中去
* 实例化SimpleCursorAdapter适配器构造函数SimpleCursorAdapter(context, layout, c, from, to)
context:当前context对象
* layout每一项的布局
* from:每一项布局中的数据映射索引数组
* to:每一项中数据对应的组件ID数组
SimpleCursorAdapter
adapter = new SimpleCursorAdapter (this, R.layout.list_item,cursor, from, to);
listview.setAdapter(adapter);// 将适配器数据映射ListView上
listview.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView&?& arg0, View arg1, int arg2,
long arg3) {
Toast.makeText(MainActivity.this,
&当前选中列表项的为第& + (arg2+1)+&列。&, Toast.LENGTH_SHORT).show();
private void insertDataIntoDB(DBHelper dbHelper) {
dbHelper.clear();
//向数据库插入数据
ContentValues values1 = new ContentValues();
values1.put(&iv&, R.drawable.list1);
values1.put(&bigtv&, &一加手机发布:强调手感 &);
values1.put(&smalltv&,
&国内手机新品牌一加手机今日在北京发布其首款产品,这是一款强调设计的手机新品,配备骁龙801处理器。16GB版售价1999.99元。&);
dbHelper.insert(values1);
ContentValues values2 = new ContentValues();
values2.put(&iv&, R.drawable.list2);
values2.put(&bigtv&, &LG L90美国发售 &);
values2.put(&smalltv&, &今日,LG L90正式在美国以T-Mobile定制机的形式进行发售,售价为228美元。&);
dbHelper.insert(values2);
这里通过DBHelper这个类来实现数据库的插入和查询功能。
package com.example.
import android.content.ContentV
import android.content.C
import android.database.C
import android.database.sqlite.SQLiteD
import android.database.sqlite.SQLiteOpenH
public class DBHelper extends SQLiteOpenHelper {
public DBHelper(Context context) {
super(context, &testDB&, null, 1);
public void onCreate(SQLiteDatabase db) {
//如果数据库不存在创建数据库tbl_test
String createTableSQL = &create table IF NOT EXISTS tbl_test &
+ &(_id integer primary key autoincrement, iv int, &
+ &bigtv text, smalltv text)&;
db.execSQL(createTableSQL);
//数据新增操作
public void insert(ContentValues values) {
SQLiteDatabase db = getWritableDatabase();
db.insert(&tbl_test&, null, values);
//游标查询数据库
public Cursor query() {
SQLiteDatabase db = getWritableDatabase();
Cursor cursor = db.query(&tbl_test&, null, null, null, null, null, null);
//清除数据库中的数据
public void clear() {
SQLiteDatabase db = getWritableDatabase();
db.delete(&tbl_test&, null, null);
//关闭读取数据库
public void close() {
SQLiteDatabase db = getWritableDatabase();
db.close();
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
自定义Adapter
使用android提供的adapter来绘制列表的话,列表的每一项的显示都是一样的。而且按钮和复选框等这些事件的组件其实是无法将数据映射在ListView上的。所以如果要***和响应按钮、复选框等组件的事件时,则需要进行自定义适配器来完成。
下面示例实现获取SD卡内的MP3格式歌曲信息通过ListView显示歌曲专辑图片、歌曲名称、歌手名,并且ListView的单双行不同颜色显示,这需要自定义adapter的子类。adapter的常用子类有BaseAdapter、ArrayAdapter、SimpleAdapter等,下面介绍自定义BaseAdapter和ArrayAdapter的实现。
1.自定义BaseAdapter
为了实现ListView的单双行不同颜色显示,需要自定义adapter的子类,下面我们实现自定义的MusicAdapter类。MusicAdapter类继承自BaseAdapter类,BaseAdapter为抽象类,继承它需要实现如下方法,因此具有较高的灵活性。
public class MusicAdapter extends BaseAdapter {
public int getCount() {
public Object getItem(int arg0) {
return null;
public long getItemId(int position) {
//实例化布局和组件以及设置组件数据
//getView(int position, View convertView, ViewGroup parent)
//position:绘制的行数
//convertView:绘制的视图,这里指的是ListView中的每一项布局
//parent:view的合集
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
return null;
ListView在绘制时首先会调用getCount()方法得到绘制次数,然后通过getView()方法一层一层进行绘制,所以我们可以在getView()方法中根据position(当前绘制的ID)来的修改绘制内容。而getItem()和getItemId()则在需要处理和取得Adapter中的数据时调用。
package com.example.
import java.util.ArrayL
import java.util.L
import android.content.ContentR
import android.content.C
import android.database.C
import android.graphics.B
import android.provider.MediaS
import android.view.LayoutI
import android.view.V
import android.view.ViewG
import android.widget.BaseA
import android.widget.ImageV
import android.widget.TextV
public class MusicAdapter
extends BaseAdapter {
private int[] colors = new int[] { 0xff3cb371, 0xffa0a0a0 };
// 用来获得ContentProvider(共享数据库)
public ContentR
// 用来装查询到的音乐文件数据
// 歌曲信息列表
public List&MusicInfo& musicL
public MusicAdapter(Context context) {
this.context =
// 取得数据库对象
cr = context.getContentResolver();
musicList = new ArrayList&MusicInfo&();
String[] mString = new String[] {
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.SIZE,
MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.DATA,MediaStore.Audio.Media._ID };
// 查询所有音乐信息
cur = cr.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mString,
null, null, null);
if (cur != null) {
// 移动游标到第一个
cur.moveToFirst();
int j = 1;
for (int i = 0; i & cur.getCount(); i++) {
if (cur.getString(0).endsWith(&.mp3&)) {// 过滤获取MP3文件
MusicInfo mInfo = new MusicInfo();
String musicName = cur.getString(0).substring(0,
cur.getString(0).lastIndexOf(&.mp3&));
mInfo.setMusicIndex(j++);
mInfo.setMusicName(musicName);
mInfo.setMusicAlubm(cur.getString(1));
mInfo.setMusicSinger(cur.getString(2));
mInfo.setMusicTime(cur.getInt(3));
mInfo.setMusicSize(cur.getInt(4));
mInfo.setMusicAlubmId(cur.getInt(5));
mInfo.setMusicPath(cur.getString(6));
mInfo.setMusicId(cur.getInt(7));
musicList.add(mInfo);
cur.moveToNext();
public int getCount() {
return musicList.size();//返回ListView项的长度
public Object getItem(int arg0) {
return musicList.get(arg0);
public long getItemId(int arg0) {
return arg0;
//实例化布局和组件以及设置组件数据
//getView(int position, View convertView, ViewGroup parent)
//position:绘制的行数
//convertView:绘制的视图,这里指的是ListView中的每一项布局
//parent:view的合集
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder();
//将布局通过LayoutInflater对象实例化为一个view
convertView = LayoutInflater.from(context).inflate(
R.layout.list_item, null);
holder.songImage = (ImageView) convertView.findViewById(R.id.listImage);
holder.singerName = (TextView) convertView.findViewById(R.id.list_Singer);
holder.songName = (TextView) convertView.findViewById(R.id.listName);
// 将holder绑定到convertView
convertView.setTag(holder);
holder = (ViewHolder) convertView.getTag();
// 向ViewHolder中填入的数据
int mid = musicList.get(position).getMusicIndex();
String musicName = musicList.get(position).getMusicName();
String musciSinger = musicList.get(position).getMusicSinger();
if (musciSinger.contains(&&unknown&&)) {
musciSinger = &&未知&&;
Bitmap img = MusicUtils.getArtwork(context,musicList.get(position).getMusicId(),musicList.get(position).getMusicAlubmId(), true);
holder.songName.setText(mid + &. & + musicName);
holder.singerName.setText(musciSinger);
holder.songImage.setImageBitmap(img);
int colorPos = position % colors.
convertView.setBackgroundColor(colors[colorPos]);
//控制背景颜色
return convertV
* ViewHolder类用以储存item中控件的引用
final class ViewHolder {
ImageView songI
TextView songN
TextView singerN
getView()方法用来获得绘制每个item的View对象,如果每次getView()被执行都new出一个View对象,长此以往会产生很大的消耗,特别当item中还有Bitmap等,甚至会造成OOM的错误导致程序崩溃。从上面的代码可以看到getView()有一个convertView参数,这个参数用来缓存View对象。当ListView滑动的过程中,会有item被滑出屏幕而不再被使用,这时候Android会回收这个item的view,这个view也就是这里的convertView。这样如果convertView不为null,就不用new出一个新的View对象,只用往convertView中填充新的item,这样就省去了new
View的大量开销。
在上面的代码中,在缓存convertView减少new View开销的同时,通过setTag()方法将数据结构ViewHolder绑定到convertView,从而利用ViewHolder存储convertView中控件对象的引用,这样避免每次调用findViewById()方法。
package com.example.
import android.app.A
import android.os.B
import android.widget.ListV
public class MainActivity extends Activity {
public MusicAdapter mA
private ListView mListV
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
* 初始化组件
private void initView() {
// 绑定XML中的ListView,作为Item的容器
mListView = (ListView) findViewById(R.id.list);
mAdapter = new MusicAdapter(MainActivity.this);
mListView.setAdapter(mAdapter);
package com.example.
import java.io.FileD
import java.io.FileNotFoundE
import java.io.IOE
import java.io.InputS
import android.content.ContentR
import android.content.ContentU
import android.content.C
import android.graphics.B
import android.graphics.BitmapF
import android.net.U
import android.os.ParcelFileD
* @author Jerryc
*音乐助手类
public class MusicUtils {
private static final Uri sArtworkUri = Uri
.parse(&content://media/external/audio/albumart&);
private static final BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options();
private static Bitmap mCachedBit = null;
//获取音乐文件专辑图片
public static Bitmap getArtwork(Context context, long song_id,
long album_id, boolean allowdefault) {
if (album_id & 0) {
// This is something that is not in the database, so get the album
// art directly
// from the file.
if (song_id &= 0) {
Bitmap bm = getArtworkFromFile(context, song_id, -1);
if (bm != null) {
if (allowdefault) {
return getDefaultArtwork(context);
return null;
ContentResolver res = context.getContentResolver();
Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
if (uri != null) {
InputStream in = null;
in = res.openInputStream(uri);
return BitmapFactory.decodeStream(in, null, sBitmapOptions);
} catch (FileNotFoundException ex) {
// The album art thumbnail does not actually exist. Maybe the
// user deleted it, or
// maybe it never existed to begin with.
Bitmap bm = getArtworkFromFile(context, song_id, album_id);
if (bm != null) {
if (bm.getConfig() == null) {
bm = bm.copy(Bitmap.Config.RGB_565, false);
if (bm == null && allowdefault) {
return getDefaultArtwork(context);
} else if (allowdefault) {
bm = getDefaultArtwork(context);
} finally {
if (in != null) {
in.close();
} catch (IOException ex) {
return null;
private static Bitmap getArtworkFromFile(Context context, long songid,
long albumid) {
Bitmap bm = null;
byte[] art = null;
String path = null;
if (albumid & 0 && songid & 0) {
throw new IllegalArgumentException(
&Must specify an album or a song id&);
if (albumid & 0) {
Uri uri = Uri.parse(&content://media/external/audio/media/&
+ songid + &/albumart&);
ParcelFileDescriptor pfd = context.getContentResolver()
.openFileDescriptor(uri, &r&);
if (pfd != null) {
FileDescriptor fd = pfd.getFileDescriptor();
bm = BitmapFactory.decodeFileDescriptor(fd);
Uri uri = ContentUris.withAppendedId(sArtworkUri, albumid);
ParcelFileDescriptor pfd = context.getContentResolver()
.openFileDescriptor(uri, &r&);
if (pfd != null) {
FileDescriptor fd = pfd.getFileDescriptor();
bm = BitmapFactory.decodeFileDescriptor(fd);
} catch (FileNotFoundException ex) {
if (bm != null) {
mCachedBit =
private static Bitmap getDefaultArtwork(Context context) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.Config.RGB_565;
return BitmapFactory.decodeStream(context.getResources()
.openRawResource(R.drawable.album), null, opts);
&?xml version=&1.0& encoding=&utf-8&?&
&LinearLayout xmlns:android=&/apk/res/android&
android:layout_width=&match_parent&
android:layout_height=&wrap_content&
android:orientation=&horizontal& &
&LinearLayout
android:layout_width=&50sp&
android:layout_height=&50sp&
android:orientation=&vertical& android:gravity=¢er& &
&ImageView
android:id=&@+id/listImage&
android:layout_width=&40sp&
android:layout_height=&40sp& /&
&/LinearLayout&
&LinearLayout
android:layout_width=&match_parent&
android:layout_height=&50sp&
android:orientation=&vertical& &
android:id=&@+id/listName&
android:layout_width=&fill_parent&
android:layout_height=&match_parent&
android:layout_weight=&1&
android:gravity=¢er_vertical&
android:paddingLeft=&10dp&
android:singleLine=&true&
android:textSize=&16sp& /&
android:id=&@+id/list_Singer&
android:layout_width=&match_parent&
android:layout_height=&match_parent&
android:layout_weight=&1&
android:gravity=¢er_vertical&
android:paddingLeft=&10dp&
android:singleLine=&true&
android:textSize=&13sp& /&
&/LinearLayout&
&/LinearLayout&
package com.example.
歌曲信息类
public class MusicInfo {
private int musicI
private int songId;//歌曲ID
private int musicAlubmId;//专辑ID
private String musicN// 歌曲名
private String musicS// 歌手名
private int musicT// 歌曲时间长度
private String musicA// 专辑名称
private int musicS// 曲歌大小
private String musicP// 歌曲路径
public int getMusicIndex() {
return musicI
public void setMusicIndex(int musicIndex) {
this.musicIndex = musicI
public int getMusicId() {
return songId;
public void setMusicId(int songId) {
this.songId = songId;
public int getMusicAlubmId() {
return musicAlubmId;
public void setMusicAlubmId(int musicAlubmId) {
this.musicAlubmId = musicAlubmId;
public String getMusicName() {
return musicN
public void setMusicName(String musicName) {
this.musicName = musicN
public String getMusicSinger() {
return musicS
public void setMusicSinger(String musicSinger) {
this.musicSinger = musicS
public int getMusicTime() {
return musicT
public void setMusicTime(int musicTime) {
this.musicTime = musicT
public String getMusicAlubm() {
return musicA
public void setMusicAlubm(String musicAlubm) {
this.musicAlubm = musicA
public int getMusicSize() {
return musicS
public void setMusicSize(int musicSize) {
this.musicSize = musicS
public String getMusicPath() {
return musicP
public void setMusicPath(String musicPath) {
this.musicPath = musicP
2.自定义ArrayAdapter&T&
在开发中需要将对象显示在listview中,这时候使用ArrayAdapter&T&来显示指定对象类型。下面自定义ArrayAdapter&T&实现上一节中自定义BaseAdapter实现的同样的效果,首先定义要显示的对象,代码参照前面的MusicInfo.class
MainActivity.java代码修改如下:
package com.example.
import java.util.ArrayL
import android.app.A
import android.content.ContentR
import android.database.C
import android.os.B
import android.provider.MediaS
import android.widget.ListV
public class MainActivity extends Activity {
public MyArrayAdapter mA
private ListView mListV
// 用来获得ContentProvider(共享数据库)
public ContentR
// 用来装查询到的音乐文件数据
// 歌曲信息列表
public ArrayList&MusicInfo& musicL
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
* 初始化组件
private void initView() {
// 取得数据库对象
cr = getContentResolver();
musicList = new ArrayList&MusicInfo&();
String[] mString = new String[] { MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.SIZE,
MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.DATA,
MediaStore.Audio.Media._ID };
// 查询所有音乐信息
cur = cr.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mString,
null, null, null);
if (cur != null) {
// 移动游标到第一个
cur.moveToFirst();
int j = 1;
for (int i = 0; i & cur.getCount(); i++) {
if (cur.getString(0).endsWith(&.mp3&)) {// 过滤获取MP3文件
MusicInfo mInfo = new MusicInfo();
String musicName = cur.getString(0).substring(0,
cur.getString(0).lastIndexOf(&.mp3&));
mInfo.setMusicIndex(j++);
mInfo.setMusicName(musicName);
mInfo.setMusicAlubm(cur.getString(1));
mInfo.setMusicSinger(cur.getString(2));
mInfo.setMusicTime(cur.getInt(3));
mInfo.setMusicSize(cur.getInt(4));
mInfo.setMusicAlubmId(cur.getInt(5));
mInfo.setMusicPath(cur.getString(6));
mInfo.setMusicId(cur.getInt(7));
musicList.add(mInfo);
cur.moveToNext();
// 绑定XML中的ListView,作为Item的容器
mListView = (ListView) findViewById(R.id.list);
mAdapter = new MyArrayAdapter(MainActivity.this, R.layout.list_item,
musicList);
mListView.setAdapter(mAdapter);
接下来自定义继承自ArrayAdapter&MusicInfo&的MyArrayAdapter类,继承ArrayAdapter&MusicInfo&只需要重写getView()方法就可以实现与上一节相同的效果,并且不用保存List&MusicInfo&对象引用。
package com.example.
import java.util.L
import android.content.C
import android.graphics.B
import android.view.LayoutI
import android.view.V
import android.view.ViewG
import android.widget.ArrayA
import android.widget.ImageV
import android.widget.TextV
public class MyArrayAdapter extends ArrayAdapter&MusicInfo& {
private int[] colors = new int[] { 0xff3cb371, 0xffa0a0a0 };
private Context mC
private int
public MyArrayAdapter(Context context, int resource,List&MusicInfo& musicList) {
super(context, resource,musicList);
this.mContext =
this.resource =
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(
resource, null);
holder.songImage = (ImageView) convertView.findViewById(R.id.listImage);
holder.singerName = (TextView) convertView.findViewById(R.id.list_Singer);
holder.songName = (TextView) convertView.findViewById(R.id.listName);
// 将holder绑定到convertView
convertView.setTag(holder);
holder = (ViewHolder) convertView.getTag();
// 向ViewHolder中填入的数据
int mid =getItem(position).getMusicIndex();
String musicName = getItem(position).getMusicName();
String musciSinger =getItem(position).getMusicSinger();
if (musciSinger.contains(&&unknown&&)) {
musciSinger = &&未知&&;
Bitmap img = MusicUtils.getArtwork(mContext,getItem(position).getMusicId(),getItem(position).getMusicAlubmId(), true);
holder.songName.setText(mid + &. & + musicName);
holder.singerName.setText(musciSinger);
holder.songImage.setImageBitmap(img);
int colorPos = position % colors.
convertView.setBackgroundColor(colors[colorPos]);
//控制背景颜色
return convertV
* ViewHolder类用以储存item中控件的引用
final class ViewHolder {
ImageView songI
TextView songN
TextView singerN
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:8012次
排名:千里之外
原创:16篇
转载:29篇
(1)(1)(1)(2)(1)(4)(2)(1)(3)(6)(15)(4)(1)(3)

参考资料

 

随机推荐