前言
前两篇已经记录了一下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();
}