前言
前兩篇已經記錄了一下Android 自定義View的原理和函數含義,這次來說說自定義View是如何實現的。其實如果說自定義View的實現方法有分類的話,應該大致分爲三種:自繪View,繼承View 和 組合View。
一、自繪View
自繪View,就是View所展示的內容都是自己繪製的,也就是都是在onDraw方法中,比如繪製一個圓:
public class MyView extends View {
private PointF mPoint = new PointF(200, 200);
private Paint mPaint;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mPaint = new Paint();
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mPoint.x, mPoint.y, 100, mPaint);
}
}
很簡單的一個例子,只是說明實現方法,很複雜炫酷的效果也都是通過這些方法實現的。
二、繼承View
繼承View,也就是我們並不需要自己重頭去實現一個控件,只需要去繼承一個現有的控件,然後在這個控件上增加一些新的功能,就可以形成一個自定義的控件了。這種自定義控件的特點就是不僅能夠按照我們的需求加入相應的功能,還可以保留原生控件的所有功能,比如 開源項目 的CircleImageView 就是繼承自ImageView 簡單分析下:
CircleImageView的主要流程:
- 首先通過setImageXxx()方法設置圖片Bitmap;
- 進入構造函數CircleImageView()獲取自定義參數,以及調用setup()函數;
- 進入setup()函數(非常關鍵),進行圖片畫筆邊界畫筆(Paint)一些重繪參數初始化:構建渲染器BitmapShader用Bitmap來填充繪製區域,設置樣式和內外圓半徑計算等,以及調用updateShaderMatrix()函數和 invalidate()函數;
- 進入updateShaderMatrix()函數,計算縮放比例和平移,設置BitmapShader的Matrix參數等;
- 觸發ondraw()函數完成最終的繪製。使用配置好的Paint先畫出繪製內圓形來以後再畫邊界圓形。
自己來自定義一個滑動到達底部自動加載更多的 RecyclerView :
public class AutoLoadRecyclerView extends RecyclerView implements LoadFinishCallBack {
private OnLoadMoreListener mLoadMoreListener;
private boolean mIsLoadingMore;
public AutoLoadRecyclerView(Context context) {
this(context, null);
}
public AutoLoadRecyclerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AutoLoadRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mIsLoadingMore = false;
setOnScrollListener(new AutoLoadScrollListener(true, true));
}
/**
* 如果需要顯示圖片,需要設置這幾個參數,快速滑動時,暫停圖片加載
*
* @param pauseOnScroll
* @param pauseOnFling
*/
public void setOnPauseListenerParams(boolean pauseOnScroll, boolean pauseOnFling) {
setOnScrollListener(new AutoLoadScrollListener(pauseOnScroll, pauseOnFling));
}
public void setmLoadMoreListener(OnLoadMoreListener mLoadMoreListener) {
this.mLoadMoreListener = mLoadMoreListener;
}
@Override
public void loadFinish() {
mIsLoadingMore = false;
}
public interface OnLoadMoreListener {
void loadMore();
}
/**
* 滑動自動加載監聽器
*/
private class AutoLoadScrollListener extends OnScrollListener {
private final boolean pauseOnScroll;
private final boolean pauseOnFling;
AutoLoadScrollListener(boolean pauseOnScroll, boolean pauseOnFling) {
super();
this.pauseOnScroll = pauseOnScroll;
this.pauseOnFling = pauseOnFling;
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
//由於GridLayoutManager是LinearLayoutManager子類,所以也適用
if (getLayoutManager() instanceof LinearLayoutManager) {
int lastVisibleItem = ((LinearLayoutManager) getLayoutManager()).findLastVisibleItemPosition();
int totalItemCount = AutoLoadRecyclerView.this.getAdapter().getItemCount();
//有回調接口,並且不是加載狀態,並且剩下2個item,並且向下滑動,則自動加載
if (mLoadMoreListener != null && !mIsLoadingMore && lastVisibleItem >= totalItemCount
- 1 && dy > 0) {
mLoadMoreListener.loadMore();
mIsLoadingMore = true;
}
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
}
}
}
大致的代碼如上,就是 繼承了 RecycleView 有個 OnScrollListener 的方法,判斷 RecycleView 滑動顯示item條數,還剩兩個沒有出現時就加載下一頁。
三、組合View
組合View 在開發中遇到的也是比較多的,不用我們去繪製每一個顯示的View,就是用系統提供的View,把需要的組合到一起成一個新的View。比如:每個頁面都有的Title,就是一個組合View形成的。Java代碼:
public class TitleView extends RelativeLayout {
private Button mBackBt;
private TextView mTitleTv;
public TitleView(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title, this);
mTitleTv = (TextView) findViewById(R.id.title_text);
mBackBt = (Button) findViewById(R.id.button_left);
mBackBt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
((Activity) getContext()).finish();
}
});
}
public void setTitleText(String text) {
mTitleTv.setText(text);
}
public void setLeftButtonText(String text) {
mBackBt.setText(text);
}
public void setLeftButtonListener(OnClickListener l) {
mBackBt.setOnClickListener(l);
}
}
如代碼所示,用了一個Button 和 TextView 組合成一個XML的layout佈局文件,然後調用了LayoutInflater的inflate()方法來加載剛剛定義的title.xml佈局。如上思路,可以用 LayoutInflater 加載佈局形成組合View的方式來自定義很多較複雜View。
總結
當然,所有的自定義View的方法都是可以自定義View的屬性的。
屬性定義在res/values/attr.xml,如:
<declare-styleable name="SearchView">
<attr name="search_height" format="integer" />
<attr name="search_version" format="enum">
<enum name="toolbar" value="1000" />
<enum name="menu_item" value="1001" />
</attr>
<attr name="search_version_margins" format="enum">
<enum name="toolbar_small" value="2000" />
<enum name="toolbar_big" value="2001" />
<enum name="menu_item" value="2002" />
</attr>
<attr name="search_theme" format="enum">
<enum name="light" value="3000" />
<enum name="dark" value="3001" />
</attr>
<attr name="search_navigation_icon" format="integer" />
<attr name="search_icon_color" format="color" />
<attr name="search_background_color" format="color" />
<attr name="search_text_color" format="color" />
<attr name="search_text_highlight_color" format="color" />
<attr name="search_text_size" format="dimension" />
</declare-styleable>
format是值該屬性的取值類型:一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag;
然後在佈局中聲明我們的自定義View:
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cl_main_out"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.young.library.customview.customsearchview.SearchView
android:id="@+id/searchView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:search_height="999"
app:search_version="menu_item" />
</android.support.design.widget.CoordinatorLayout>
在View的構造方法中獲取我們的自定義樣式:
/**
* 獲得我自定義的樣式屬性
*
* @param context
* @param attrs
* @param defStyle
*/
public CustomView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
/**
* 獲得我們所定義的自定義樣式屬性
*/
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SearchView, defStyle, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{
int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.SearchView_search_text_color:
mTitleTextColor = a.getColor(attr, Color.BLACK);
break;
case R.styleable.SearchView_search_text_size:
mTitleTextSize = a.getDimension(attr, 0);
break;
}
}
a.recycle();
}