在Android開發中,大家經常會提到自定義控件的問題,對於好多初學者來說,可以說談之色變,其實自定義控件並沒有那麼難,下面我就帶大家通過寫一個自定義控件—–通訊錄右側的導航字母,來解釋一下自定義控件的使用。
在解釋之前先給大家看一下運行的具體效果,由於我不會截取動態圖,所以就普通圖片給大家看一下啦,我們要實現的就是如下圖中右側的字母導航,我們可以點擊右側的某個字母來直接快速查找首字母爲該字母的內容,如圖:
首先我們自定義類繼承View類,這裏需要注意,我們需要實現其三個構造方法;如下:
這三個構造方法的用途可以參考註釋的內容,大家可以在自己練習的時候,把其中的一個或者兩個刪掉,然後運行看看會報什麼錯誤,然後根據錯誤提示,就可以找到該構造函數的用途。
接下來就是在該自定義控件中繪製自己想要的內容;在這篇博客中,我們要繪製的是右側的導航字母,其實難度並不大,代碼如下:
public class ListViewSideBar extends View{
/**
* 右側導航欄顯示的字母的數組
*/
private String[] mIndexers ;
/**
* 聲明畫筆
*/
private Paint mTextPaint ;
/**
* 文字的顏色的字符串
*/
private String mTextColorString ;
/**
* 文字的尺寸
*/
private float mTextSize ;
/**
* 文字的高度,根據現有高度與需要顯示的文字的數量計算得到
*/
private float mTextHeight ;
/**
* 文字的尺寸相關數據封裝對象,用於計算繪製文字時垂直方向的偏移量
*/
private Paint.FontMetrics mFontMetrics ;
/**
* 控件的寬度
*/
private int mViewWidth ;
/**
* 控件的高度
*/
private int mViewHeight ;
/**
* 構造方法,將在JAVA程序中創建對象時被調用
* @param context
*/
public ListViewSideBar(Context context) {
this(context, null);
}
/**
* 構造方法,將在使用res\layout設計佈局時被調用
* @param context
* @param attrs
*/
public ListViewSideBar(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化
init() ;
}
/**
* 構造方法,將在使用res\layout設計佈局時被調用
* @param context
* @param attrs
* @param defStyleAttr
*/
public ListViewSideBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 初始化
*/
private void init() {
// 加載需要被顯示的文字
mIndexers = getResources().getStringArray(R.array.sort_key_array) ;
// 初始化文字大小
mTextSize = 16f ;
// 初始化文字顏色
mTextColorString = "#666666" ;
// 初始化畫筆
mTextPaint = new Paint() ;
mTextPaint.setAntiAlias(true) ;
mTextPaint.setTextAlign(Paint.Align.CENTER) ;
mTextPaint.setTextSize(mTextSize) ;
mTextPaint.setColor(Color.parseColor(mTextColorString)) ;
// 根據畫筆,得到文字尺寸相關的數據
mFontMetrics = mTextPaint.getFontMetrics() ;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 獲取當前控件的尺寸
mViewWidth = getMeasuredWidth() ;
mViewHeight = getMeasuredHeight() ;
// 計算每個文字佔據的高度
mTextHeight = mViewHeight / mIndexers.length ;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪製文字時水平方向的偏移量,由於畫筆已經設置水平居中,則該偏移量爲控件寬度的一半
float x = mViewWidth / 2 ;
// 繪製文字垂直方向的偏移量
float y ;
for (int i = 0; i < mIndexers.length; i++) {
// 計算當前繪製的文字在垂直方向的偏移量,注:以下代碼並沒有實現文字在可用空間內垂直居中
y = i * mTextHeight + mFontMetrics.ascent * -1 ;
// 執行繪製
canvas.drawText(mIndexers[i], x, y, mTextPaint) ;
}
}
}
這是關於繪製的內容,在重寫onDraw方法時,大家一定要注意的是,不要輕易的刪掉父類的onDraw方法的引用super.onDraw(canvas),除非你能保證你不需要用到父類的方法,否則會報錯。
然後我們會想,一個自定義的控件功能也不至於這麼單調吧,我們在通訊錄中可以通過點擊右側的字母來快速查找首字母爲被點擊的字母的姓名,接下來我們也用這個功能來實現一下:
/**
* 當某個Indexer被點擊時的監聽器對象
*/
private OnIndexerClickListener mOnIndexerClickListener ;
/**
* 設置當某個Indexer被點擊時的監聽器對象
*
* @param listener
* 監聽器對象
*/
public void setOnIndexerClickListener(OnIndexerClickListener listener) {
this.mOnIndexerClickListener = listener ;
}
/**
* 當點擊某個字母時的監聽器
*/
public static interface OnIndexerClickListener {
/**
*
* 當字母導航中的文字被按下時,該方法被自動回調
*
* @param position 被按下的文字在列表中的索引
* @param str 被按下的文字
*/
void onIndexerClick(int position, String str) ;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 判斷觸屏操作的類型是否是按下
if(event.getAction() == MotionEvent.ACTION_DOWN) {
// 獲取當前按下的位置
float y = event.getY() ;
// 計算當前按下時對應的文字的下標
int index = (int) (y / mTextHeight) ;
// 獲取當前按下時的文字
String str = mIndexers[index] ;
// 回調監聽器方法
if(mOnIndexerClickListener != null) {
mOnIndexerClickListener.onIndexerClick(index, str) ;
return true ;
}
}
return super.onTouchEvent(event);
}
接下來就是在需要顯示該控件的界面上來調用了:
public class MainActivity extends AppCompatActivity implements ListViewSideBar.OnIndexerClickListener {
private List<Student> students ;
private MyAdapter adapter ;
private ListView lvShow ;
private ListViewSideBar sideBar ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
lvShow = (ListView) findViewById(R.id.lv_listview) ;
students = new ArrayList<Student>() ;
adapter = new MyAdapter(students, this) ;
lvShow.setAdapter(adapter) ;
sideBar = (ListViewSideBar) findViewById(R.id.lvsb_sort_keys) ;
sideBar.setOnIndexerClickListener(this) ;
new LoadDataTask().execute() ;
}
@Override
public void onIndexerClick(int position, String str) {
int moveToPosition = adapter.getPositionForSection(str.charAt(0)) ;
lvShow.setSelectionFromTop(moveToPosition, 0) ;
}
private class LoadDataTask extends AsyncTask<Object, Object, Object> {
@Override
protected Object doInBackground(Object... params) {
//獲取數據
List<Student> data = StudentDaoFactory.newInstance().getStudentList() ;
// 排序
Collections.sort(data, new Comparator<Student>() {
@Override
public int compare(Student lhs, Student rhs) {
return lhs.getSortKey().compareTo(rhs.getSortKey()) ;
}
});
return data ;
}
@Override
protected void onPostExecute(Object result) {
List<Student> data = (List<Student>) result ;
students.addAll(data) ;
adapter.notifyDataSetChanged() ;
}
}
}
以下是Adapter適配器的代碼(關於adapter我在這裏不做詳細介紹了,以代碼爲例):
public class MyAdapter extends BaseAdapter implements SectionIndexer {
private List<Student> students ;
private Context context ;
public MyAdapter(List<Student> students, Context context) {
this.context = context;
setData(students) ;
}
private void setData(List<Student> students) {
if(students == null) {
students = new ArrayList<Student>() ;
}
this.students = students ;
}
@Override
public int getCount() {
return students.size() ;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder ;
if(convertView == null) {
holder = new ViewHolder() ;
convertView = LayoutInflater.from(context).inflate(R.layout.listview_item, null) ;
holder.tvName = (TextView) convertView.findViewById(R.id.tv_item_name) ;
holder.tvSortKey = (TextView) convertView.findViewById(R.id.tv_item_sortKey) ;
convertView.setTag(holder) ;
} else {
holder = (ViewHolder) convertView.getTag();
}
Student student = students.get(position) ;
holder.tvName.setText(student.getName()) ;
holder.tvSortKey.setText(getFirstChar(student.getSortKey())) ;
// 使用SectionIndex的解決方案:判斷是否顯示首字母
// 1. 獲取當前Student的sortKey的首字母
int section = getSectionForPosition(position) ;
// 2. 獲取當前首字母應該出現的位置
int sectionPos = getPositionForSection(section) ;
// 3. 判斷當前getView()時的position,是否與當前section應該出現的位置相符
if(position == sectionPos) {
holder.tvSortKey.setVisibility(View.VISIBLE) ;
} else {
holder.tvSortKey.setVisibility(View.GONE) ;
}
return convertView ;
}
private class ViewHolder {
private TextView tvName ;
private TextView tvSortKey ;
}
/**
* 獲取當前位置的首字母
* @param sortKey
* @return
* 當前位置的首字母
*/
private CharSequence getFirstChar(String sortKey) {
return sortKey.substring(0, 1).toUpperCase(Locale.CHINA) ;
}
@Override
public int getPositionForSection(int section) {
// 爲首字母獲取位置,即:根據首字母確定該首字母在數據集中的位置
int position = 0 ;
for(int i = 0; i < students.size(); i++) {
// 根據當前循環到的i,獲取對應的首字母
int currentChar = getSectionForPosition(i) ;
// 判斷當次循環到的首字母是否與參數相等,如果相等,則可以確定參數對應的字母應該出現的位置
if(currentChar == section) {
position = i ;
break ;
}
}
return position ;
}
@Override
public int getSectionForPosition(int position) {
// 獲取當前位置的首字母
return students.get(position).getSortKey().toUpperCase(Locale.CHINA).charAt(0) ;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public Object[] getSections() {
return new Object[0];
}
}
具體的實體類和數據的操作類我在這裏就不貼代碼了,不要在這裏跟我講你還不會獲取數據。
當看完這篇文字你是否發現,其實自定義控件並不是那麼難,對於我們想要實現的控件,只要我們能想得到,我們就可以把它畫出來,而且也可以將一些其他的邏輯處理全部封裝在我們的自定義類裏面,就像本例中的字母的點擊監聽邏輯,我們也可以在這裏爲其設置其他更多的邏輯,當你需要使用的時候,直接調用即可。
最後總結一下自定義控件的實現步驟:
1、自定義類繼承View類,並實現其三個構造方法;
2、重寫onDraw()方法,繪製自己需要繪製的控件;
3、可根據需要添加一些其他處理邏輯(如點擊變化、滑動變化等一系列效果);
4、在佈局文件中聲明該控件;
5、在需要顯示的界面初始化,併爲其添加需要實現的邏輯;
希望這篇文字能夠對大家有所幫助,其實自定義控件這個知識點是相當大的,不是一篇博客能夠解釋清楚,具體就需要大家在平時的使用中去探索了,另外,我下面爲大家附上一個鏈接,大家有興趣的可以參考一下愛哥的自定義控件的博客。