本篇主要以自定义简单的 RecycleView 为例,带我们走进自定义 view 的世界,为了与原生的RecycleView区分这里我将我的简易版定义为ZlRecycleView以免大家搞混了
简介
自定义view有三种
- 继承view 的控件
- 继承ViewGroup控件容器
- 组合view
主要方法
onMeasure() ,onLayout() ,ondraw()
事件处理方法
onInterceptTouchEvent() ,dispatchTouchEvent(),onTouchEvent(),scrollBy()
开始自定义
主要功能实现
ZlRecycleView 毫无疑问需要继承自ViewGroup,它可以容纳各种各样的View
控件,它的item可以复用所以我们也给他定义了回收池(ZlRecycle),当 item
滑出屏幕给它添加到回收池,底下item 加载到屏幕时先看回收池中有没有缓存,
有的话复用,没有的话创建、绑定数据展示
自定义详解
- 准备工作
//ZlRecycleView
public class ZlRecycleView extends ViewGroup {
//自定义 view 必不可少的构造函数,传递上下文和属性
public ZlRecycleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
//recycleview 需要的 Adapter
public interface Adapter{
View onCreateViewHodler(int position , View convertView , ViewGroup parent);
View onBindViewHodler(int position , View convertView , ViewGroup parent);
int getItemViewType(int row);
int getViewTypeCount();
int getCount();
int getHeight(int i);
}
}
public class ZlRacycle {
// 回收池 栈数组 ,多少种view类型就有多少栈
private Stack<View>[] views;
//
public ZlRacycle (int count) { //缓存多少种view
views = new Stack[count];
for (int i = 0; i <count ; i++) {
views[i] = new Stack<View>();
}
}
// 取
public View getRecycleView(int type){
// 取那个类型的view用 type识别
try{
return views[type].pop();
}catch (Exception e) {
return null;
}
}
// 存
public void addRecycleVeiew(int type,View view){
// 取那个类型的view用type识别
views[type].push(view);
}
}
以上代码中定义了一个自定义view类和回收池类比较简单,
回收池使用栈方便存取
- 对ZlRacycleView初始化
private void init(Context context){
needRelayout = true;
//初始化屏幕中展示的View
viewList = new ArrayList<>();
//获得最小滑动距离
ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
touchSlop = viewConfiguration.getScaledTouchSlop();
}
- 在layout 中摆放childview
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//遍历childview 重新摆放childview 位置 耗时操作尽量避免
// needRelayout boolean类型 判断调用时是否需要重新摆放childView
if(needRelayout || changed){ // 父控件改变或者需要重新摆放childview
needRelayout = false;
viewList.clear();
removeAllViews();
if(adapter != null ){
//childview 的数量
rowcount = adapter.getCount();
heights = new int[rowcount];
for (int i = 0; i < rowcount; i++) {
heights[i] += adapter.getHeight(i); //累计view的高度
}
withd = r -1 ;
height = b -1;
int top = 0,bottom ;
for (int i = 0; i < rowcount && top< height; i++) {
//view从上向下显示,第一个view的bottom 相当于第二个view的top
bottom = top + heights[i];
//创建新的childview或者从缓存池里获取
View view = makeAndSetup(i, l , r, t,b );
viewList.add(view);
top = bottom;
}
}
}
}
private View makeAndSetup(int indexData, int l, int r, int t , int b) {
View view = botain(indexData , r-l , b -t );
//计算好后需要调用view.layout 摆放childview 的位置
view.layout(l ,t ,r, b);
return view;
}
private View botain(int row , int width , int height) {
//先从回收池里找
int type = adapter.getItemViewType(row);
View recycleView = recycle.getRecycleView(type);
View view ;
if(recycleView == null){
//回收池没有,创建
view = adapter.onCreateViewHodler(row ,null, this );
if (view == null) {
throw new RuntimeException("onCreateViewHodler must be init");
}
} else {
view = adapter.onBindViewHodler(row , recycleView , this);
}
if(view == null){
throw new RuntimeException("onCreateViewHodler view = null");
}
view.setTag(R.id.tag_type_view , type);
//设置MeasureSpec 由view的宽高和 MeasureSpec的状态组成
view.measure(MeasureSpec.makeMeasureSpec(width , MeasureSpec.EXACTLY) ,
MeasureSpec.makeMeasureSpec(height , MeasureSpec.EXACTLY) );
addView(view , 0);
return view;
}
- 移除childview 时添加到缓存池
@Override
public void removeView(View view) {
super.removeView(view);
//移除 view从屏幕中移除添加到回收池
int type = (int) view.getTag(R.id.tag_type_view );
recycle.addRecycleVeiew(type , view);
}
- 事件拦截和消费
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//事件冲突 滑动时recycleview 消费 点击时itemview消费
//滑动拦截
boolean intercapt = false; // 是否再滑动
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//按下时距离屏幕的顶端的距离
currentY = (int) ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int y2 = Math.abs(currentY(int)ev.getRawY());
//按下移动时大于最小移动距离要拦截到时间给onTouchEvent()消费
if (y2 > touchSlop) {
intercapt = true;
}
break;
}
return intercapt;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
//消费滑动时间 判断上滑还是下滑
int y2 = (int) ev.getRawY();
// 从上往下滑是负值 从下往上滑是正值
int diff = currentY - y2;
scrollBy(0 , diff);
break;
}
return super.onTouchEvent(ev);
}
- 滑动事件
@Override
public void scrollBy(int x, int diff) {
scrollY += diff;
if(scrollY > 0){
//向上滑
//firstRow屏幕中第一个childview再viewlist中的position
//滑动高度大于第一个view的高度说明它花出去了,将它移除
while (heights[firstRow] < scrollY){
if (!viewList.isEmpty()) {
removeView(viewList.remove(0));
}
//重置scrollY 和 firstRow
scrollY -= heights[firstRow];
firstRow ++ ;
}
}
//获取屏幕中第一个view到最后的一个view的高度,小于recycleview 的高度 就需要创建新view或者从缓存中取出一个展示
//因为有缓存池 所以加载一屏后 viewList 的size就是固定的了 firstRow + size就是下一个添加的view的索引
while (getfilledHeight() < height){
final int size = viewList.size();
int dataIndex = firstRow + size;
View view = botain(dataIndex , withd,heights[dataIndex]);
viewList.add(view);
}
// repositionViews();
}
private int getfilledHeight() {
return sumArray(heights , firstRow , viewList.size()) - scrollY;
}
private int sumArray(int array[], int firstIndex , int count){
int sum = 0;
count+= firstIndex;
for (int i = firstIndex; i<count;i++){
sum += array[i];
}
return sum;
}
完结
以上文字叙述比较简单,主要代码都添加了注释,哪有不对忘指正。
没有用到onMeasure 和 onDraw 方法 不知道大家发现了嘛,这两个方法也是自定义view中重要的方法。onMeasure 用来测量自身宽高 ,onDraw 可以再画布中绘制想要的图案,这里以后再单独介绍。