編輯:關於Android編程
雖然隨著RecyclerView
的不斷普及,相應的資源也越來越多,許多的項目都在使用RecyclerView
,但作為他的前輩ListView
,加深對ListView
的使用有助於我們更好的適應到RecyclerView
的使用中。
首先看一下我們實現的效果一些簡單效果
這只是前面的一些簡單效果,後面會有一些進階的效果,希望能耐心的看下去。
ListView
的優化主要包括兩個方面,分別是對自身的優化以及其適配器(Adapter
)的優化。
ListView
自身的優化主要包括一條。對於ListView
的layout_height
和layout_width
設置為match_parent
,如果設置為match_parent
,一般ListView
的寬高會測量三次以上。具體的源碼沒有深入研究。但為什麼會要測量多次,如果對於自定義View
稍微有點基礎的會知道,對於View
的測量大小有三個類型:
- UNSPECIFIED
:未指定的,父類不對子類施加任何限制。
- EXACTLY
:確定的,父類確定其子類控件的大小。
- AT_MOST
:最大值,需要子類去測量自身大小確定。
如果我們設置寬高為wrap_content
,即AT_MOST
,表示其寬高有控件本身去測量確定,而如果是match_parent
,則EXACTLY
,表示確定的大小。
Adapter
優化。對於Adapter的優化,主要包括以下幾個步驟:
復用convertView
,減少子布局的生成。
定義ViewHolder
,減少findViewById()
的次數。
下面看一下代碼
/**
* 最基礎的adapter
* Created by Alex_MaHao on 2016/5/17.
*/
public class SimpleBaseAdapter extends BaseAdapter {
private List datas;
public SimpleBaseAdapter(List datas) {
this.datas = datas;
}
@Override
public int getCount() {
return datas==null?0:datas.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder vHolder;
if(convertView==null){
//初始化item布局
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_listview_sample,parent,false);
//創建記事本類
vHolder = new ViewHolder();
//查找控件,保存控件的引用
vHolder.tv = ((TextView) convertView.findViewById(R.id.listview_sample_tv));
//將當前viewHolder與converView綁定
convertView.setTag(vHolder);
}else{
//如果不為空,獲取
vHolder = (ViewHolder) convertView.getTag();
}
vHolder.tv.setText(datas.get(position));
return convertView;
}
/**
* 筆記本類,保存對象的引用
*/
class ViewHolder{
TextView tv;
}
}
根據代碼分析思路:
- ListView
中的item
滑出屏幕時,滑出的item
會在getView()
方法中返回,及converView
參數。所以在這裡判斷,是否為null,如果不為null,表示我們可以對該布局重新設置數據並返回到列表中顯示。
- ViewHolder
,保存item
中子控件的引用。因為我們復用了converView
,那麼對於同一個converView
布局,其子控件的引用應該是不變的。所以我們可以獲取到其上的所有子控件的引用並通過ViewHolder
進行保存。
- setTag(),getTag()
:該方法實現了ViewHolder
和converView
的綁定,就類似於通過setTag()
方法,將ViewHolder
打包成一個包裹,放在了converView
上,再通過getTag()
方法,將這個包裹取出。
在很多優化中,會將
ViewHolder
定義為static
。但在這裡我並沒加上。因為經過測試,加或不加,ViewHolder
的創建次數不變。網上查了很多資料,也沒有找到一個讓我信服的理由。唯一有點理的就是基於java的特性。靜態內部類的對象不依賴於其所在的外部類對象。
上一節說了Adapter
的優化,但如果我們每次寫都要寫這麼多的優化代碼,這不符合程序員懶惰的天性。那我們只能把他封裝,提取出一個公共的基類,在基類中,我們把布局加載,優化等都默認實現,只需讓子類構造布局文件,以及綁定數據。
那麼,從我們上一節的代碼看,有以下模塊都可以提取為基類:
數據集合datas:對於數據集合,我們通常都是一個List
集合,在這裡定義泛型來表述其所包含的內容。 數據優化:布局的復用以及ViewHolder
與converView
的綁定。 ViewHolder
:定義一個ViewHolder
,通過map
保存控件與id;
子類所需實現的:
數據的初始化 確定item
的布局文件 將數據與視圖綁定。
那麼直接看一下我們繼承好的代碼:
public abstract class BaseAppAdapter extends BaseAdapter {
/**
* 泛型,保存數據
*/
protected List datas;
/**
* 構造方法,子類必須實現其構造方法,並初始化數據
* @param datas
*/
public BaseAppAdapter(List datas) {
this.datas = datas;
}
@Override
public int getCount() {
return datas==null?0:datas.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
BaseViewHolder vHolder;
if(convertView==null){
convertView = LayoutInflater.from(parent.getContext()).inflate(getItemLayoutId(),parent,false);
vHolder = new BaseViewHolder(convertView);
convertView.setTag(vHolder);
}else{
vHolder = (BaseViewHolder) convertView.getTag();
}
/**
* 數據綁定的回調
*/
bindData(vHolder,datas.get(position));
return convertView;
}
/**
* 子類實現,獲取item布局文件的id
* @return
*/
protected abstract int getItemLayoutId();
/**
* 子類實現,綁定數據
* @param vHolder 對應position的ViewHolder
* @param data 對應的數據綁定
*/
protected abstract void bindData(BaseViewHolder vHolder,T data);
/**
* ViewHolder類
*/
class BaseViewHolder{
/**
* 保存view,以id-view的形式
*/
Map mapView;
View rootView;
public BaseViewHolder(View rootView){
this.rootView = rootView;
mapView = new HashMap();
}
/**
* 通過id查找控件
* @param id
* @return
*/
public View getView(Integer id){
View view = mapView.get(id);
if(view==null){
view = rootView.findViewById(id);
mapView.put(id,view);
}
return view;
}
}
}
在以上代碼中有幾個關鍵點:
有參的構造方法:子類實現時,必須調用父類的構造方法,用以保存數據。getItemLayoutId()
:item
的布局文件id。抽象方法,子類必須實現。對於每一個BaseAppAdapter
的子類,都需要通過該方法返回他們所特有的item
的id。 bindData(BaseViewHolder vHolder,T data)
:數據綁定方法,子類必須實現。子類在此方法中將數據設置到試圖上 BaseViewHolder
中的getView(Integer id)
:該方法設計比較巧妙,因為,對於基類我們不知道有哪些id
需要查詢,如果直接通過findViewById()
方法,則並沒有減少查詢次數。在這裡,通過保存一個map
對象,通過id
先從map
中取,如果不存在,說明是第一次獲取,則使用findViewById
查找控件,並將控件以鍵值對的形式保存到map
中,則下次查找,會從map
中取,減少了findViewById
的次數。可能有人會認為,添加了一個map
對象,內存占用不是增大了嗎,但是,第一,我們map
中保存的都是引用。第二,findViewById()
是按照深度優先遍歷查詢的,如果不懂,遍歷肯定能明白吧。
簡單的兩個屬性
android:divider
:設置顏色,背景 android:dividerHeight
:設置高度
如果設置無分割線,可以設置android:divider="@null"
。
該兩個屬性必須同時使用,如果只設置
divider
,則沒有效果,同時默認的分割線也會消失。
當然,我們也可以在item
布局文件中添加分割線(在底部添加一個線),麻煩點而已。
android:scrollbars="none"
對於ListView
,在android5.0一下是一個變色,在android5.0以上是一個波紋動畫。我們可以通過一些設置取消掉他的反饋效果。使用如下代碼
android:listSelector="#0000"
:點擊顏色設置為透明
ListView
作為列表顯示的控件,那麼肯定會存在數據為空的情況,如果我們就直接顯示一面白無疑是不友好的,ListView
可以通過設置一個視圖,當列表為null,顯示該視圖。
關鍵方法為setEmpty(View)
。具體使用方法如下:
xml文件中,在ListView
下設置一個圖片,如下
在java代碼中設置如下
//設置空視圖,調用此方法會默認隱藏Empty_view
mLv.setEmptyView(findViewById(R.id.listview_empty_view));
注意:我們無需設置
ImageView
的visible
屬性,因為通過setEmpty()
方法,已將其顯示與隱藏於ListView
所綁定,由ListView
來管理。
ListView
的滾動對於顯示數據,我們可能會有一些特殊的要求,比如初始顯示到第幾列,或者要調到第幾條顯示。對於此種要求,ListView
已經實現了相應的方法讓我調用。
public void setSelection(int position)
:position
為需要顯示在第一條的數據。該方法為瞬間滾動,無滑動效果,類似圖片上瞬間滑動按鈕實現的效果。 public void smoothScrollToPosition(int position)
:與上面方法作用相同,不過其有過度效果,類似平滑滾動實現的效果。
在ListView
中,有如下方法,他控制了回彈設置的值:
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent)
maxOverScrollY
參數便是設置我們可以下拉的空白區域的高度。具體實現如下
public class MyListView extends ListView {
/**
* 下拉回彈效果,下拉的最大距離
*/
private int mMaxOverDistance;
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
//初始化最大距離
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
float density = metrics.density;
mMaxOverDistance = (int) (100*density);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
//注意第九個參數,設置為了我們自定義的值
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mMaxOverDistance, isTouchEvent);
}
}
該方法是保護類型的,所以我們需要通過繼承ListView
來修改此方法的實現。
在看到此方法時,第一反應是ScrollView
有沒有該方法,查了以後,發現ScrollView
也有該方法。那麼,同理,ScrollView
也能通過重寫overScrollBy
實現下拉回彈的效果。
首先看一下效果圖:
分析其邏輯:
當我們向上滑動列表時,toolbar
慢慢消失到頂部。 當我們向下滑動列表時,toolbar
慢慢的從頂部顯現。
實現邏輯:
- setOnTouchListener
,設置觸摸監聽。
- 對當前手指移動事件進行判斷。
- 如果是向下移動,且ToolBar
是隱藏的,則給toolbar
設置一個向下顯現的動畫。
- 如果是向上移動,且toolbar
是顯現的,則給toolbar
設置一個向上隱藏的動畫。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPr+00rvPwsq1z9a0+sLro7o8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;">
/**
* Created by Alex_MaHao on 2016/5/17.
*/
public class ToorBarListViewActivity extends AppCompatActivity implements View.OnTouchListener {
private Toolbar mToolBar;
private ListView mLv;
private SimpleBaseAdapter mAdapter;
private List
代碼注釋非常詳細,需要注意的有以下三點:
因為我們的父布局是RelativeLayout
,如果不給ListView
設置一個headView
,則在最頂部時,ListView
頂部數據會被隱藏。當然,可能會想到為什麼不用垂直布局,把ListView
放在toolbar
的下面呢,這樣會導致另一個問題,即當toobar
消失時,會導致頂部有一個空白效果。如下圖所示:
監聽兩個條件,滑動方向和toolbar
當前顯示的狀態,如果不監聽當前toobar
狀態,會導致動畫重復調用。
會存在一種情況,及toolbar
處在某一個動畫的時候,突然該表方向,我們需要先停止當前動畫,並以當前偏移量上為基礎,創建新的動畫。這也是創建動畫時使用mToolBar.getTranslationY()
作為參數的原因。
在顯示數據中,可能會有這種需求,顯示的列表的布局不同,例如聊天界面,根據不同的聊天人,顯示的位置不同,及加載不同的itemView
那麼我們如何實現呢。先看一下實現效果:
請忽略他的丑陋,目的是為了實現效果,下面直接上源碼,看一下實現過程,最後在總結實現的關鍵點:
聊天的實體類,在聊天實體類中,我們必須通過類型區分該聊天內容是自己還是朋友。
/**
* 聊天的實體類
* Created by Alex_MaHao on 2016/5/18.
*/
public class ChatBean {
/**
* 聊天的兩種類型,自己和朋友
*/
public static final int CHAT_MYSELF = 0;
public static final int CHAT_FIRENDS = 1;
/**
* 聊天的頭像
*/
private Drawable userIcon;
/**
* 發送的消息
*/
private String message;
/**
* 確定是否為當前聊天者
*/
private int type;
public ChatBean() {
}
public ChatBean(String message, int type, Drawable userIcon) {
this.message = message;
this.type = type;
this.userIcon = userIcon;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public Drawable getUserIcon() {
return userIcon;
}
public void setUserIcon(Drawable userIcon) {
this.userIcon = userIcon;
}
}
ListView
所在的布局文件activity_listview_chat.xml
因為聊天條目沒有點擊反饋效果,所以通過android:listSelector
設置無反饋效果。使用android:divider="#0000"
和android:dividerHeight="10dp"
設置每一個聊天條目的間隔。
我們需要加載不同的布局,所以我們有兩個item
布局文件,用來顯示自己的消息和朋友的消息:
item_listview_chat_left
,顯示在左邊,朋友消息顯示布局
item_listview_chat_right
,顯示在右邊,自己消息的顯示布局
關鍵點來了,構造ChatAdapter
,用以顯示數據。
/**
* 聊天的Adapter,實現加載不同的布局
* Created by Alex_MaHao on 2016/5/18.
*/
public class ChatAdapter extends BaseAdapter {
List chatBeens ;
public ChatAdapter(List chatBeens) {
this.chatBeens = chatBeens;
}
@Override
public int getCount() {
return chatBeens.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public int getItemViewType(int position) {
//返回值需要從0開始,對應數據所要加載的對應布局標識
return chatBeens.get(position).getType();
}
@Override
public int getViewTypeCount() {
//不同布局的種類數量
return 2;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//獲取當前數據的類型
int type = getItemViewType(position);
ViewHolder vHolder = null;
if(convertView==null){
switch (type){
case ChatBean.CHAT_MYSELF:
//加載自己消息顯示的布局
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_listview_chat_right,parent,false);
vHolder = new ViewHolder();
vHolder.icon = ((ImageView) convertView.findViewById(R.id.chat_right_icon));
vHolder.msg = ((TextView) convertView.findViewById(R.id.chat_right_msg));
break;
case ChatBean.CHAT_FIRENDS:
//加載朋友消息顯示的布局
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_listview_chat_left,parent,false);
vHolder = new ViewHolder();
vHolder.icon = ((ImageView) convertView.findViewById(R.id.chat_left_icon));
vHolder.msg = ((TextView) convertView.findViewById(R.id.chat_left_msg));
break;
}
//控件應用與控件綁定
convertView.setTag(vHolder);
}else{
//獲取控件引用
vHolder = (ViewHolder) convertView.getTag();
}
//設置數值
vHolder.icon.setImageDrawable(chatBeens.get(position).getUserIcon());
vHolder.msg.setText(chatBeens.get(position).getMessage());
return convertView;
}
class ViewHolder{
ImageView icon;
TextView msg;
}
}
在adapter
中有兩個關鍵的方法:
getViewTypeCount()
:返回布局的種類數,比如當前我們分為自己的消息和朋友的消息兩個布局,那麼就返回2.
-getItemViewType()
:返回對應條目下,item
布局的類型。ListView
通過該方法,實現在getView()
中返回相同類型的converView
復用。注意,該返回類型要從0開始,不然會數組越界。
因為在這裡,比較巧合,我們的item
布局控件都一樣,只不過顯示不一樣,所以只定義了一個ViewHolder
類。
關鍵的實現了,那麼看一下activity
中的代碼吧
public class ChatListViewActivity extends AppCompatActivity {
private ListView mChatLv;
private List chatBeanList = new ArrayList<>();
private ChatAdapter adapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_listview_chat);
mChatLv = ((ListView) findViewById(R.id.listview_chat_lv));
initChatBeanList();
adapter = new ChatAdapter(chatBeanList);
mChatLv.setAdapter(adapter);
mChatLv.postDelayed(new Runnable() {
@Override
public void run() {
//設置我們ListView顯示在底部
mChatLv.setSelection(adapter.getCount());
}
},200);
}
@Override
protected void onStart() {
super.onStart();
}
/**
* 初始化聊天內容
*/
private void initChatBeanList() {
for (int i = 0;i<10;i++){
ChatBean chat = new ChatBean();
if(i%2==0){
chat.setType(ChatBean.CHAT_MYSELF);
chat.setUserIcon(getApplicationContext().getResources().getDrawable(R.mipmap.qq));
chat.setMessage("微信,微信,我是QQ");
}else{
chat.setType(ChatBean.CHAT_FIRENDS);
chat.setUserIcon(getApplicationContext().getResources().getDrawable(R.mipmap.weixin));
chat.setMessage("QQ,QQ,我是微信");
}
chatBeanList.add(chat);
}
}
}
實現該布局的兩個關鍵方法(adapter中的方法)
-getViewTypeCount()
:返回布局的種類數,比如當前我們分為自己的消息和朋友的消息兩個布局,那麼就返回2.
-getItemViewType()
:返回對應條目下,item
布局的類型。ListView
通過該方法,實現在getView()
中返回相同類型的converView
復用。注意,該返回類型要從0開始,不然會數組越界。
決定使用多種方式實現下拉刷新和上拉加載,會在下一篇博客中實現。絕對滿滿的干貨。
該項目源碼已經共享到我的github,在模塊
systemwidgetdemo
中。
一、Android中的事件處理方法事件處理:響應用戶UI動作,提高應用程序交互性1、基於監聽的事件處理機制2、基於回調的事件處理機制3、Handler消息處理前面我們已經
Activity的生命周期 Android的核心組件 1.Viiew :界面 ,組織UI控件 2.Intent :意圖,支持組件之間的通信 3.Activity:
本文實例講述了Android實現將一個Activity設置成窗口樣式的方法。分享給大家供大家參考,具體如下:1.在res/value文件夾下的style.xml文件中加入
Android 中的 Service按運行地點分類:1、本地服務(Local) 該服務依附在主進程上, 服務依附在主進程上而不是獨立的進程,這樣在一定程度上節約了資源,另