横向ListView(一) ——开篇,基础逻辑实现
2019独角兽企业重金招聘Python工程师标准>>>
第一次写博文,写得不好的地方还望各位看客见谅
为了学习自定义软件开发,且定制出满足自己需求的控件(不需要将就地使用第三方源码),本人花了一周的时间开发了个横向ListView,写博客是为了记录整个开发过程及思路,也能和各位看客一起学习和探讨。
这一系列文章是针对的读者是已经了解listview缓存和工作原理的android开发人员,如果对listview缓存和工作原理还不了解的读者,可以查看以下文章:
《Android研究院之ListView原理学习与优化总结》
目前横向ListView的可替代方案有以下三种:
1.HorizontalScrollView——android官方提供
2.RecyclerView——android6.0提供的
3.第三方开源控件
尽管有众多的选择,但感觉还是自己会实现比较酷一些,还有就是,自己的东西可以随便改改改改改。
本篇文章将介绍横向ListView的实现基本思路,在接下来的一系列文章中将不断介绍整个控件的完善思路(包括:实现快速滚动、添加头/尾视图、添加滚动条、实现下拉刷新/上拉加载等)。
参考文章: 《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 View 事件传递》
《《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;
转载于:https://my.oschina.net/u/3614895/blog/1438912
横向ListView(一) ——开篇,基础逻辑实现相关推荐
- 横向ListView(四) —— 添加滚动条
2019独角兽企业重金招聘Python工程师标准>>> 在前面的文章已经介绍了横向ListView的基础实现及头尾视图的添加等的实现,这篇文章将介绍为横向ListView添加滚动条: ...
- 张高兴的 UWP 开发笔记:横向 ListView
ListView 默认的排列方向是纵向 ( Orientation="Vertical" ) ,但如果我们需要横向显示的 ListView 怎么办? Blend for Visua ...
- 简单的横向ListView实现(version 3.0)
版本号2仅仅是简单的实现了当手指按下的时候listView的Item向左移动一定的距离,并没有随着手指的左右移动而左右滚动.在这个版本号3.0中将会实现随着手指的移动而滚动的目标:当手指向左移动的时候 ...
- Android横向ListView功能实现
--在开发类似新闻功能的App过程中需要在新闻页面展示新闻附件,附件一多的话一行展示不完,换行又不好看,为此需要做像今日头条导航栏那样可以横向滑动的列表,不同点在于附件数量是动态添加的 可能有一个 也 ...
- 商业世界的五大基础逻辑
商业世界万般复杂,创业者更是九死一生,才能在千军万马的商业世界中脱颖而出.商业世界也有其基础的逻辑.我认为简单的理解就是客户来源.价值差额.数量大小.风险管控.规则漏洞这五部分.客户来源就是你的用户是 ...
- 商业的基础逻辑(一)
商业的基础逻辑 业务成功=用户价值*商业价值*组织实现 用户价值:公司有没有看清楚用户需求和选对满足需求的方式 商业价值:指公司是否有清晰的战略思路.增长路径和竞争策略 组织实现:公司有没有合适的团队 ...
- HorizontalListView 横向listview
tag:HorizontalListView 横向listview 仿 优酷 播放列表 由于优酷的客户端很酷,最近在做视频列表的时候客户要求做出类似效果,开始打算用Gallery的,不过后来发现横向 ...
- ET框架的基础逻辑,生命周期和Scene层级树
ET框架的基础逻辑 文章目录 ET框架的基础逻辑 ECS思想和OOP思想的区别 ECS下简易的逻辑的分发 ET框架下实体的生命周期 ET框架的Scene树 ECS思想和OOP思想的区别 以传统RP ...
- Android UI开发: 横向ListView(HorizontalListView)及一个简单相册的完整实现 (附源码下载)
本文内容: 1.横向ListView的所有实现思路; 2.其中一个最通用的思路HorizontalListView,并基于横向ListView开发一个简单的相册: 3.实现的横向ListView在点击 ...
最新文章
- MySQL数据库介绍、安装(服务端软件安装、客户端软件安装(图形化界面客户端和命令行客户端))
- MySQL: ERROR 1040: Too many connections”的异常情况1
- 轻易致盲分类器!普渡大学提出光学对抗攻击算法:OPAD,想法奇特,性能有效!...
- android.view.WindowManager$BadTokenException
- GBK转unicode码查询表的改进
- 性能之巅:Linux网络性能分析工具
- [ISSUE]invalid 'cobj' in function 'lua_cocos2dx_EventDispatcher_dispatchCustomEvent'
- [原创]升级SOUI WKE以支持_blank
- linux 分区格式化类型,Linux分区格式化
- 计算机网络安装,计算机网络系统安装操作指南.pdf
- Spring boot整合Drools、flowable决策引擎解决方案
- ffmpeg学习日记701-报错-co located POCs unavailable
- 熊猫人表情包python 代码_Python实现表情包的代码实例
- 【C语言】取余%操作在编程中的重要作
- 50种认知偏差要注意,这样才能做最好的自己
- vs2019 fatal error C1090: PDB API “3“
- 通联互联网支付网关商户接口技术规范
- 既然有公平锁,为什么还要有非公平锁
- 点歌系统服务器有什么作用,KTV里的点歌系统用什么服务器
- IE8 未知的运行时错误(ueditor编辑器在ie8、ie7下出现JS错误的解决方法)
热门文章
- c语言中如何设计和编写一个应用系统?
- AjaxFileUpload文件上传组件(php+jQuery+ajax)
- 使用dom4j解析XML例子
- 第十周项目5:贪心的富翁
- 数据库开发基本操作-安装Sql Server 2005出现“性能监视器计数器要求”错误解决方法...
- Swift基础 - - 高德地图实践
- 力扣(LeetCode)933
- zabbix专题:第十一章 zabbix之SNMP方式监控
- 用Servlet获取表单数据
- 用excel表格做好客户关系管理