第一次写博文,写得很差的地方还望各位看客见谅html
为了学习自定义软件开发,且定制出知足本身需求的控件(不须要将就地使用第三方源码),本人花了一周的时间开发了个横向ListView,写博客是为了记录整个开发过程及思路,也能和各位看客一块儿学习和探讨。java
这一系列文章是针对的读者是已经了解listview缓存和工做原理的android开发人员,若是对listview缓存和工做原理还不了解的读者,能够查看如下文章:android
《Android研究院之ListView原理学习与优化总结》api
目前横向ListView的可替代方案有如下三种:缓存
1.HorizontalScrollView——android官方提供ide
2.RecyclerView——android6.0提供的布局
3.第三方开源控件学习
尽管有众多的选择,但感受仍是本身会实现比较酷一些,还有就是,本身的东西能够随便改改改改改。优化
本篇文章将介绍横向ListView的实现基本思路,在接下来的一系列文章中将不断介绍整个控件的完善思路(包括:实现快速滚动、添加头/尾视图、添加滚动条、实现下拉刷新/上拉加载等)。ui
参考文章: 《Android UI开发: 横向ListView(HorizontalListView)及一个简单相册的完整实现》
横向ListView的基础逻辑:
1.新建java类,类名:HorizontalListView
2.继承AdapterView
3.实现setAdapter()和getAdapter()方法(须要为adapter注册数据观察器)
4.实现onTouchEvent()方法响应事件(采用android提供的手势解析器GestureDetector解析事件)
5.实现onLayout方法,布局列表项
1).计算当前列表发生滚动的滚动“位移值”,记录已经发生有效滚动的“位移累加值”
2).根据“位移值”提取须要缓存的视图(已经滚动到可视区域外的列表项)
3).根据“位移值”设置须要显示的的列表项
4).根据总体列表“显示偏移值”整顿全部列表项位置(调用子view的列表项)
5).计算能够发生滚动的“最大位移值”
先上代码:
package com.hss.os.horizontallistview.history_version; import android.content.Context; import android.database.DataSetObserver; import android.os.Build; import android.support.annotation.RequiresApi; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.widget.AdapterView; import android.widget.ListAdapter; import java.util.LinkedList; import java.util.Queue; /** * 横向ListView的基础逻辑 * 1.继承AdapterView * 2.实现setAdapter()和getAdapter()方法(须要为adapter注册数据观察器) * 3.实现onTouchEvent()方法响应事件(采用android提供的手势解析器GestureDetector解析事件) * 4.实现onLayout方法,布局列表项 1).计算当前列表发生滚动的滚动“位移值”,记录已经发生有效滚动的“位移累加值” 2).根据“位移值”提取须要缓存的视图(已经滚动到可视区域外的列表项) 3).根据“位移值”设置须要显示的的列表项 4).根据总体列表“显示偏移值”整顿全部列表项位置(调用子view的列表项) 5).计算能够发生滚动的“最大位移值” * * Created by hss on 2017/7/17. */ public class HorizontalListView1 extends AdapterView<ListAdapter> { private ListAdapter adapter = null; private GestureDetector mGesture; private Queue<View> cacheView = new LinkedList<>();//列表项缓存视图 private int firstItemIndex = 0;//显示的第一个子项的下标 private int lastItemIndex = -1;//显示的最后的一个子项的下标 private int scrollValue=0;//列表已经发生有效滚动的位移值 private int hasToScrollValue=0;//接下来列表发生滚动所要达到的位移值 private int maxScrollValue=Integer.MAX_VALUE;//列表发生滚动所能达到的最大位移值(这个由最后显示的列表项决定) private int displayOffset=0;//列表显示的偏移值(用于矫正列表显示的全部子项的显示位置) public HorizontalListView1(Context context) { super(context); init(context); } public HorizontalListView1(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public HorizontalListView1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public HorizontalListView1(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context); } private void init(Context context){ mGesture = new GestureDetector(getContext(), mOnGesture); } private void initParams(){ removeAllViewsInLayout(); if(adapter!=null&&lastItemIndex<adapter.getCount()) hasToScrollValue=scrollValue;//保持显示位置不变 else hasToScrollValue=0;//滚动到列表头 scrollValue=0;//列表已经发生有效滚动的位移值 firstItemIndex = 0;//显示的第一个子项的下标 lastItemIndex = -1;//显示的最后的一个子项的下标 maxScrollValue=Integer.MAX_VALUE;//列表发生滚动所能达到的最大位移值(这个由最后显示的列表项决定) displayOffset=0;//列表显示的偏移值(用于矫正列表显示的全部子项的显示位置) requestLayout(); } private DataSetObserver mDataObserver = new DataSetObserver() { @Override public void onChanged() { //执行Adapter数据改变时的逻辑 initParams(); } @Override public void onInvalidated() { //执行Adapter数据失效时的逻辑 initParams(); } }; @Override public ListAdapter getAdapter() { return adapter; } @Override public void setAdapter(ListAdapter adapter) { if(adapter!=null){ adapter.registerDataSetObserver(mDataObserver); } if(this.adapter!=null){ this.adapter.unregisterDataSetObserver(mDataObserver); } this.adapter=adapter; requestLayout(); } @Override public View getSelectedView() { return null; } @Override public void setSelection(int position) { } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); /* 1).计算当前列表发生滚动的滚动“位移值”,记录已经发生有效滚动的“位移累加值” 2).根据“位移值”提取须要缓存的视图(已经滚动到可视区域外的列表项) 3).根据“位移值”设置须要显示的的列表项 4).根据总体列表“显示偏移值”整顿全部列表项位置(调用子view的列表项) 5).计算能够发生滚动的“最大位移值” */ int dx=calculateScrollValue(); removeNonVisibleItems(dx); showListItem(dx); adjustItems(); calculateMaxScrollValue(); } /** * 计算这一次总体滚动偏移量 * @return */ private int calculateScrollValue(){ int dx=0; hasToScrollValue=hasToScrollValue<0? 0:hasToScrollValue; hasToScrollValue=hasToScrollValue>maxScrollValue? maxScrollValue:hasToScrollValue; dx=hasToScrollValue-scrollValue; scrollValue=hasToScrollValue; return -dx; } /** * 计算最大滚动值 */ private void calculateMaxScrollValue(){ if(adapter==null) return; if(lastItemIndex==adapter.getCount()-1) {//已经显示了最后一项 if(getChildAt(getChildCount() - 1).getRight()>=getShowEndEdge()) { maxScrollValue = scrollValue + getChildAt(getChildCount() - 1).getRight() - getShowEndEdge(); }else{ maxScrollValue = 0; } } } /** * 根据偏移量提取须要缓存视图 * @param dx */ private void removeNonVisibleItems(int dx) { if(getChildCount()>0) { //移除列表头 View child = getChildAt(getChildCount()); while (getChildCount()>0&&child != null && child.getRight() + dx <= 0) { displayOffset += child.getMeasuredWidth(); cacheView.offer(child); removeViewInLayout(child); firstItemIndex++; child = getChildAt(0); } //移除列表尾 child = getChildAt(getChildCount()-1); while (getChildCount()>0&&child != null && child.getLeft() + dx >= getShowEndEdge()) { cacheView.offer(child); removeViewInLayout(child); lastItemIndex--; child = getChildAt(getChildCount()-1); } } } /** * 根据偏移量显示新的列表项 * @param dx */ private void showListItem(int dx) { if(adapter==null)return; int firstItemEdge = getFirstItemLeftEdge()+dx; int lastItemEdge = getLastItemRightEdge()+dx; displayOffset+=dx;//计算偏移量 //显示列表头视图 while(firstItemEdge > getPaddingLeft() && firstItemIndex-1 >= 0) { firstItemIndex--;//往前显示一个列表项 View child = adapter.getView(firstItemIndex, cacheView.poll(), this); addAndMeasureChild(child, 0); firstItemEdge -= child.getMeasuredWidth(); displayOffset -= child.getMeasuredWidth(); } //显示列表未视图 while(lastItemEdge < getShowEndEdge() && lastItemIndex+1 < adapter.getCount()) { lastItemIndex++;//日后显示一个列表项 View child = adapter.getView(lastItemIndex, cacheView.poll(), this); addAndMeasureChild(child, getChildCount()); lastItemEdge += child.getMeasuredWidth(); } } /** * 调整各个item的位置 */ private void adjustItems() { if(getChildCount() > 0){ int left = displayOffset+getPaddingLeft(); int endIndex = getChildCount()-1; for(int i=0;i<=endIndex;i++){ View child = getChildAt(i); int childWidth = child.getMeasuredWidth(); child.layout(left, getPaddingTop(), left + childWidth, child.getMeasuredHeight()+getPaddingTop()); left += childWidth + child.getPaddingRight(); } } } /** * 取得视图可见区域的右边界 * @return */ private int getShowEndEdge(){ return getWidth()-getPaddingRight(); } private int getFirstItemLeftEdge(){ if(getChildCount()>0) { return getChildAt(0).getLeft(); }else{ return 0; } } private int getLastItemRightEdge(){ if(getChildCount()>0) { return getChildAt(getChildCount()-1).getRight(); }else{ return 0; } } private void addAndMeasureChild(View child, int viewIndex) { LayoutParams params = child.getLayoutParams(); params = params==null ? new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT):params; addViewInLayout(child, viewIndex, params, true); child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED)); } /** * 在onTouchEvent处理事件,让子视图优先消费事件 * @param event * @return */ @Override public boolean onTouchEvent(MotionEvent event) { return mGesture.onTouchEvent(event); } private GestureDetector.OnGestureListener mOnGesture = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { synchronized(HorizontalListView1.this){ hasToScrollValue += (int)distanceX; } requestLayout(); return true; } }; }
如下是具体实现解析:
第1-3步是总体实现的准备工做,比较简单,这里就不作讲解
4.实现onTouchEvent()方法响应事件(采用android提供的手势解析器GestureDetector解析事件)
处理触摸事件的方法有三个(如下说法针对当前使用的GestureDetector实现):
1.dispatchTouchEvent() —— 若是在这里处理,子视图和当前视图能够同时响应事件
2.onInterceptTouchEvent() —— 若是在这里处理,子视图没法响应事件
3.onTouchEvent() —— 优先子视图响应事件
以上三个方法涉及到事件分发机制,若是对这方面不是很懂也想学习的,可参考如下文章:
《《Android深刻透析》之Android事件分发机制 》
在实现GestureDetector.OnGestureListener时,必需实现onDown()和onScroll()两个方法
onScroll()方法用于获取用户的滚动行为所产生的滚动值
onDown()方法必须实现且返回值必须是true,不然onScroll()方法没法执行,具体缘由还未深究
5.实现onLayout方法,布局列表项
1).计算当前列表发生滚动的滚动“位移值”,记录已经发生有效滚动的“位移累加值”
private int calculateScrollValue(){ int dx=0; hasToScrollValue=hasToScrollValue<0? 0:hasToScrollValue; hasToScrollValue=hasToScrollValue>maxScrollValue? maxScrollValue:hasToScrollValue; dx=hasToScrollValue-scrollValue; scrollValue=hasToScrollValue; return -dx; }
在这里采用了三个变量:
private int scrollValue=0;//列表已经发生有效滚动的位移值
private int hasToScrollValue=0;//接下来列表发生滚动所要达到的位移值
private int maxScrollValue=Integer.MAX_VALUE;//列表发生滚动所能达到的最大位移值(这个由最后显示的列表项决定)
在这个时候就有个问题,为何要采用这三个变量而不是直接使用用户滚动行为所产生的偏移值(onScroll()方法中的distanceX);直接使用distanceX去计算也是能够实现咱们所须要的功能的,不过这样处理起来,各部分的逻辑代码耦合度就会很高,没法切分出各个步骤,这个对于代码的维护工做带来很大的不便,代码的可读性也很差,逻辑也不够清晰,采用这三个变量能很好的解决以上问题(这个思路是借用别人的,具体是谁最初想到的,我也不清楚,不过挺佩服的)
2).根据“位移值”提取须要缓存的视图(已经滚动到可视区域外的列表项)
/** * 根据偏移量提取须要缓存视图 * @param dx */ private void removeNonVisibleItems(int dx) { if(getChildCount()>0) { //移除列表头 View child = getChildAt(getChildCount()); while (getChildCount()>0&&child != null && child.getRight() + dx <= 0) { displayOffset += child.getMeasuredWidth(); cacheView.offer(child); removeViewInLayout(child); firstItemIndex++; child = getChildAt(0); } //移除列表尾 child = getChildAt(getChildCount()-1); while (getChildCount()>0&&child != null && child.getLeft() + dx >= getShowEndEdge()) { cacheView.offer(child); removeViewInLayout(child); lastItemIndex--; child = getChildAt(getChildCount()-1); } } }
这一步是在列表发生滚动以后根据发生滚动的位移值dx计算滚动后第一个和最后一个列表项是否已经滚动到不可见的区域(注意:可见的区域宽度 =(控件的宽度 - 左padding - 右padding))
3).根据“位移值”设置须要显示的的列表项
/** * 根据偏移量显示新的列表项 * @param dx */ private void showListItem(int dx) { if(adapter==null)return; int firstItemEdge = getFirstItemLeftEdge()+dx; int lastItemEdge = getLastItemRightEdge()+dx; displayOffset+=dx;//计算偏移量 //显示列表头视图 while(firstItemEdge > getPaddingLeft() && firstItemIndex-1 >= 0) { firstItemIndex--;//往前显示一个列表项 View child = adapter.getView(firstItemIndex, cacheView.poll(), this); addAndMeasureChild(child, 0); firstItemEdge -= child.getMeasuredWidth(); displayOffset -= child.getMeasuredWidth(); } //显示列表未视图 while(lastItemEdge < getShowEndEdge() && lastItemIndex+1 < adapter.getCount()) { lastItemIndex++;//日后显示一个列表项 View child = adapter.getView(lastItemIndex, cacheView.poll(), this); addAndMeasureChild(child, getChildCount()); lastItemEdge += child.getMeasuredWidth(); } }
这一步根据列表滚动的“位移值dx”计算是否须要在列表中添加新的item View,若是列表在移动的过程当中,第一个显示的item View的左边界出如今总体视图可见区域的左边界内即(firstItemEdge > getPaddingLeft() ),则在列表头添加一个新的item View,同时记录下整个列表显示的左边偏移值(displayOffset -= child.getMeasuredWidth(); ),该值十分重要,是体现整个列表显示状态的值;若是最后一个显示的item View的右边界出如今总体视图可见区域的右边界内即(lastItemEdge < getShowEndEdge() ) ,则在列表尾添加一个新的item View;第一次显示列表时,是以追加的方式显示item View的
注意:
1.代码中采用while() {}循环操做而不是采用if()直接判断是为了代码逻辑的严密性,实际上这里采用if()进行判断操做效果是同样的,可这样作整个代码的逻辑就不够严密,可能在之后的扩展中留下隐患(bug),在removeNonVisibleItems(int dx)方法中的while操做也是基于以上考虑
2.firstItemEdge 和lastItemEdge 的值采用如下方法计算,不只是为了加强代码的可读性,更是为了日后的扩展作准备
private int getFirstItemLeftEdge(){ if(getChildCount()>0) { return getChildAt(0).getLeft(); }else{ return 0; } } private int getLastItemRightEdge(){ if(getChildCount()>0) { return getChildAt(getChildCount()-1).getRight(); }else{ return 0; } }
4).根据总体列表“显示偏移值”整顿全部列表项位置(调用子view的列表项)
/** * 调整各个item的位置 */ private void adjustItems() { if(getChildCount() > 0){ int left = displayOffset+getPaddingLeft(); int top = getPaddingTop(); int endIndex = getChildCount()-1; int childWidth,childHeight; for(int i=0;i<=endIndex;i++){ View child = getChildAt(i); childWidth = child.getMeasuredWidth(); childHeight = child.getMeasuredHeight(); child.layout(left, top, left + childWidth, top + childHeight); left += childWidth; } } }
在这里是对视图项进行正确的布局排列,把各个列表项安放到合适的位置上;这个列表如何显示,整体依赖displayOffset这个值;值得注意的是,child.layout()中的right和bottom的值须要在宽和高的基础上分别加上left和top的值,不然整个item View没法彻底显示。
5).计算能够发生滚动的“最大位移值”
/** * 计算最大滚动值 */ private void calculateMaxScrollValue(){ if(adapter==null) return; if(lastItemIndex==adapter.getCount()-1) {//已经显示了最后一项 if(getChildAt(getChildCount() - 1).getRight()>=getShowEndEdge()) { maxScrollValue = scrollValue + getChildAt(getChildCount() - 1).getRight() - getShowEndEdge(); }else{ maxScrollValue = 0; } } }
当列表滚动到最后一个列表项时,则可计算整个列表可滚动最大值;scrollValue 表示已经发生滚动的距离,getChildAt(getChildCount() - 1).getRight() - getShowEndEdge()表示还能够发生滚动的距离,也表示最后一个列表项(item View)未显示出来的部分;若是显示项过少而没法铺满整个控件,最大滚动位移值为0,即maxScrollValue = 0;