ListView的簡單使用
編輯布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:orientation="vertical"> <ListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/listview"/> </LinearLayout>
接下來修改MainActivity代碼
class MainActivity : AppCompatActivity() { private val data= listOf("數據1","數據2","數據1","數據1","數據1","數據1","數據1","數據1","數據1","數據1") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // supportActionBar?.hide() val adapter=ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data) listview.adapter=adapter } }
可以看到這里添加了一個initFruits方法。用於初始化數據。
其中 repeat是Kotlin相對於Java新加入的特性,取代for(int i=0;i<5;i++)
用於簡單的重復工作。所以這里有兩個循環。呃。。不過不要在意這些細節。
因為ListView一般是用來顯示大量數據的,所以我們應該先將數據准備好。這些數據一般是從網上獲取,或者讀取本地數據庫,但是這里為了方便就直接創建一個集合就好了。
不過,集合中的數據無法直接傳遞給ListView。我們需要借助適配器來完成。
使用simple_list_item_1只能單獨顯示一段數據,顯然不符合我們在開發過程中的需求。因此我們需要進行對ListView的界面進行定制。
接下來,我們定義一個水果(Fruit)實體類。
class Fruits(val name:String,val imageId:String) { }
水果類中有兩個字段,名字和圖片。
然后我們為ListView 的子項指定一個自定義的布局。在Layout先新建一個fruit_item.xml的文件。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="60dp"> <ImageView android:layout_width="35dp" android:layout_height="35dp" android:id="@+id/fruitImage" android:layout_gravity="center" android:layout_marginLeft="10dp"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/fruitName" android:layout_gravity="center_vertical" android:layout_margin="10dp"/> </LinearLayout>
接下來創建一個適配器FruitAdapter
代碼如下
class FruitAdapter(activity: Activity,val resourceId:Int,data:List<Fruits>) : ArrayAdapter<Fruits>(activity,resourceId,data){ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view =LayoutInflater.from(context).inflate(resourceId,parent,false) val fruitImage:ImageView=view.findViewById(R.id.fruitImage)//綁定布局得圖片 val fruitName:TextView=view.findViewById(R.id.fruitName)//綁定布局中得名字 val fruit=getItem(position)//獲取當前項得Fruit實例 if (fruit!=null){ fruitImage.setImageResource(fruit.imageId) fruitName.text=fruit.name } return view } }
在使用getView中,使用Layout Inflater來為這個子項價值我們傳入得布局。Layout Inflater 的inflater()接收三個參數,第三個參數設置為false 表示只讓我們在父布局中聲明的layout屬性生效。但不會為這個View添加父布局。因為View一旦有了父布局它就就不能添加
到ListView中。
最后修改MainActivity中的代碼。
class MainActivity : AppCompatActivity() { private val fruits=ArrayList<Fruits>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // supportActionBar?.hide() initFruits() //初始化列表數據 val adapter=FruitAdapter(this,R.layout.fruit_item,fruits) listview.adapter=adapter } private fun initFruits(){ repeat(2){ for (i in 0..20){ fruits.add(Fruits("測試一",R.mipmap.ic_launcher)) } } } }
如何提升ListView 的運行效率。這個問題,去面試的時候經常會問到。
目前我們的List View運行效率是很低的。因為在FruitAdapter的getView()方法中,每次都將布局重新加載一遍。當ListView快速滾動的時候,就會成為性能的瓶頸。
getView中還有個convertView的參數。這個參數用於將之前的加載好的布局進行緩存。以便之后進行重用。我們可以借助這個參數進行性能優化。接下來修改這個適配器的代碼:
class FruitAdapter(activity: Activity,val resourceId:Int,data:List<Fruits>) : ArrayAdapter<Fruits>(activity,resourceId,data){ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { var view:View if (convertView==null){ view=LayoutInflater.from(context).inflate(resourceId,parent,false) }else{ view=convertView } val fruitImage:ImageView=view.findViewById(R.id.fruitImage)//綁定布局得圖片 val fruitName:TextView=view.findViewById(R.id.fruitName)//綁定布局中得名字 val fruit=getItem(position)//獲取當前項得Fruit實例 if (fruit!=null){ fruitImage.setImageResource(fruit.imageId) fruitName.text=fruit.name } return view } }
我們對getView中的方法進行了判斷,如果converView為空,則私用LayoutInflater去加載布局,如果不為空則對convertVIew進行重用。這樣可以提高ListView的效率。但是現在每次在getView中還是會調用View 中的findViewById方法來獲取控件實例。
我們可以借助一個ViewHolder來對這部分性能進行優化。繼續修改FruitAdapter中代碼。如下:
class FruitAdapter(activity: Activity,val resourceId:Int,data:List<Fruits>) : ArrayAdapter<Fruits>(activity,resourceId,data){ inner class ViewHolder(val fruitImage:ImageView,val fruitName:TextView) override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { var view:View val viewHolder:ViewHolder if (convertView==null){ view=LayoutInflater.from(context).inflate(resourceId,parent,false) val fruitImage:ImageView=view.findViewById(R.id.fruitImage)//綁定布局得圖片 val fruitName:TextView=view.findViewById(R.id.fruitName)//綁定布局中得名字 viewHolder=ViewHolder(fruitImage,fruitName) view.tag=viewHolder }else{ view=convertView viewHolder=view.tag as ViewHolder } val fruit=getItem(position)//獲取當前項得Fruit實例 if (fruit!=null){ viewHolder.fruitImage.setImageResource(fruit.imageId) viewHolder.fruitName.text=fruit.name } return view } }
可以看到我們新增一個ViewHolder的內部類。用於對子項布局的控件進行實例緩存。
經過這番操作,ListView的運行效率提升了不少。
除此之外還可以
4.將ListView的scrollingCache和animateCache設置為false
scrollingCache: scrollingCache本質上是drawing cache,你能夠讓一個View將他自己的drawing保存在cache中(保存為一個bitmap),這樣下次再顯示View的時候就不用重畫了,而是從cache中取出。默認情況下drawing cahce是禁用的。由於它太耗內存了,可是它確實比重畫來的更加平滑。
而在ListView中,scrollingCache是默認開啟的,我們能夠手動將它關閉。
animateCache: ListView默認開啟了animateCache,這會消耗大量的內存,因此會頻繁調用GC,我們能夠手動將它關閉掉
優化前的ListView
<ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="wrap_content" android:cacheColorHint="#00000000" android:divider="@color/list_background_color" android:dividerHeight="0dp" android:listSelector="#00000000" android:smoothScrollbar="true" android:visibility="gone" />
優化后:
<ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="wrap_content" android:divider="@color/list_background_color" android:dividerHeight="0dp" android:listSelector="#00000000" android:scrollingCache="false" android:animationCache="false" android:smoothScrollbar="true" android:visibility="gone" />
ListVIew的點擊事件。
使用setOnItemClickListener()方法注冊一個監聽器,用戶點擊時,回調到Lambda表達式中,通過position參數判斷用戶點擊哪個item
在MainActivity中修改,代碼如下:
class MainActivity : AppCompatActivity() { private val fruits=ArrayList<Fruits>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // supportActionBar?.hide() initFruits() //初始化列表數據 val adapter=FruitAdapter(this,R.layout.fruit_item,fruits) listview.adapter=adapter listview.setOnItemClickListener { parent,view,position,id -> val fruit=fruits[position] Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show() } } private fun initFruits(){ repeat(2){ for (i in 0..20){ fruits.add(Fruits("測試一",R.mipmap.ic_launcher)) } } } }
重新運行,就可以看到,每點擊一個item,就會彈出一個Toast提升你點擊的那個item的name。
觀察上面代碼,我們發現聲明4個參數,但我們只用了一個,這個時候我們可以將item的監聽事件改寫為
listview.setOnItemClickListener { _,_ ,position,_ -> val fruit=fruits[position] Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show() }
更強大的滾動控件 RecyclerView
recyclerView 可以輕松實現LIstView的效果,還優化lListVIew的各種不足之處。
在app/build.gradle 中添加一行依賴,將RecyclerView添加到項目中。點擊同步
implementation 'androidx.recyclerview:recyclerview:1.1.0'
在activity_main.xml修改
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:orientation="vertical"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
這里為了方便就直接在原來代碼修改了,只是將ListVIew換成RecyclerView。item項就用原來的。
接下來,我們創建一個RecyclerView的適配器。
class FruitAdapter2(val fruitList:List<Fruits>):RecyclerView.Adapter<FruitAdapter2.ViewHolder>(){ inner class ViewHolder(view:View):RecyclerView.ViewHolder(view){ val fruitImage:ImageView=view.findViewById(R.id.fruitImage) val fruitName:TextView=view.findViewById(R.id.fruitName) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view =LayoutInflater.from(parent.context).inflate(R.layout.fruit_item,parent,false) return ViewHolder(view) } override fun getItemCount()=fruitList.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { val fruit=fruitList[position] holder.fruitImage.setImageResource(fruit.imageId) holder.fruitName.text=fruit.name } }
這里定義了一個內部類 ViewHolder,它要繼承RecyclerView.ViewHolder、ViewHolder的主構造函數要傳入一個View參數。這個參數通常是RecyclerView子項的最外層布局。我們可以通過findViewById來獲取布局控件的Id。
繼承RecyclerView.Adapter就必須重寫onCreateViewHolder()、onBindVIewHolder()和getItemCount()這三個方法。
onCreateViewHolder() | 加載布局文件,創建ViewHolder實例 |
onBindVIewHolder() | 對RecyclerView的子項進行賦值 |
getItemCount() | 直接告訴RecyclerView有多少子項,直接返回數據長度即可 |
隨后將MainActivity代碼改成
class MainActivity : AppCompatActivity() { private val fruits=ArrayList<Fruits>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // supportActionBar?.hide() initFruits() //初始化列表數據 // val adapter=FruitAdapter(this,R.layout.fruit_item,fruits) // listview.adapter=adapter // listview.setOnItemClickListener { _,_ ,position,_ -> // val fruit=fruits[position] // Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show() // } val layoutManager=LinearLayoutManager(this) recyclerView.layoutManager=layoutManager val adapter=FruitAdapter2(fruits) recyclerView.adapter=adapter } private fun initFruits(){ repeat(2){ for (i in 0..20){ fruits.add(Fruits("測試一",R.mipmap.ic_launcher)) } } } }
此時運行效果和ListView一樣,就不放圖。
實現橫向滾動和瀑布流布局
ListVIew 只能實現縱向滾動效果,橫向是做不到的,如果我們需要橫向滾動就需要借助RecyclerView了。
我們只需要將fruit_item進行修改如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="80dp" android:layout_height="wrap_content" android:orientation="vertical" > <ImageView android:layout_width="35dp" android:layout_height="35dp" android:id="@+id/fruitImage" android:layout_gravity="center_horizontal" android:layout_marginTop="10dp"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/fruitName" android:layout_gravity="center_horizontal" android:layout_marginTop="10dp"/> </LinearLayout>
然后在MainActivity中修改:
class MainActivity : AppCompatActivity() { private val fruits=ArrayList<Fruits>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // supportActionBar?.hide() initFruits() //初始化列表數據 // val adapter=FruitAdapter(this,R.layout.fruit_item,fruits) // listview.adapter=adapter // listview.setOnItemClickListener { _,_ ,position,_ -> // val fruit=fruits[position] // Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show() // } val layoutManager=LinearLayoutManager(this) recyclerView.layoutManager=layoutManager layoutManager.orientation=LinearLayoutManager.HORIZONTAL val adapter=FruitAdapter2(fruits) recyclerView.adapter=adapter } private fun initFruits(){ repeat(2){ for (i in 0..20){ fruits.add(Fruits("測試一",R.mipmap.ic_launcher)) } } } }
layoutManager.orientation=LinearLayoutManager.HORIZONTAL
可以看到這里只添加了一行就實現了,效果如下:
那么接下來實現瀑布流布局(瀑布流的效果好像多用於短視頻一類的比較多),還是來改fruit_item.xml如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_margin="5dp" > <ImageView android:layout_width="35dp" android:layout_height="35dp" android:id="@+id/fruitImage" android:layout_gravity="center_horizontal" android:layout_marginTop="10dp"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/fruitName" android:layout_gravity="left" android:layout_marginTop="10dp"/> </LinearLayout>
瀑布流布局應該是根據布局寬度來進行適配2,而不是固定值,所以將80dp改為match_parent。
將MainActive改為
class MainActivity : AppCompatActivity() { private val fruits=ArrayList<Fruits>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // supportActionBar?.hide() initFruits() //初始化列表數據 // val adapter=FruitAdapter(this,R.layout.fruit_item,fruits) // listview.adapter=adapter // listview.setOnItemClickListener { _,_ ,position,_ -> // val fruit=fruits[position] // Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show() // } val layoutManager=StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL) recyclerView.layoutManager=layoutManager // layoutManager.orientation=LinearLayoutManager.HORIZONTAL val adapter=FruitAdapter2(fruits) recyclerView.adapter=adapter } private fun initFruits(){ repeat(2){ for (i in 0..20){ fruits.add(Fruits(getRandomLengthString("測試文字"),R.mipmap.ic_launcher)) } } } private fun getRandomLengthString(str:String):String{ val n=(1..20).random() val builder=StringBuilder() repeat(n){ builder.append(str) } return builder.toString() } }