自定义控件02
自定义控件
①,纯粹自定义绘制
②,在原生的基础上追加功能.
1,下拉刷新功能(继承ListView追加功能)(下拉刷新,加载更多,两个功能)
1.1 下拉刷新
①创建一个类,继承ListView
创建自定义适配器,设置数据
额外:自定义控件会放到view包下
②自定义控件的头(即下拉的时候显示的view)
推荐名称initHeaderView();在构造方法中初始化这个头
this.addHeaderView()//添加一个头布局的控件,在listView顶部添加一个头
头部ui参考,左边的小箭头在刷新的时候是一个圆环滚动(Progress Bar)可以考虑用帧布局实现(松开的时候显示为ProgressBar)
③创建一个自定义颜色的ProgressBar
创建xml文件,先写rotate节点(旋转动画)观察可知,转圈是从0-360度旋转
参照物以自身为中心.
在这个xml文件中再写一个rotate的同级节点shape,
shapes属性:ring环形
(不过安卓下是全是方形,实际上是一个正方形背景透明,指定了一个环形)
属性innerRadiusRatio=”内半径比”
环形的半径指定的正方形宽/半径比
属性thicknessRatio=”厚度比”
环形的厚度>>指定的宽度/厚度比(可以看做外层一圈环形)
useLevel=”false”//不停旋转
子节点gradient 渐变色(推荐灰色为主,指定开始,中间,结束颜色)
安卓下实际上环形头为结束颜色,尾巴的颜色为开始颜色
属性type=”sweep”// 三种颜色扫过
最后把shape整个节点放到rotate节点下,之前放在同级,是为了避免编写时候报错
④隐藏头布局(通过设置paddingTop为负数即可,这个负数为头布局的高度)
头布局的高度一定要设置为wrapcontent包裹内容
headerView.getHXX()//获取高度的时候,如果控件没有显示到界面是获取不到的.
//获得一个测量后的高度,只有在测量之后才能获取到.
headerView.getMeasuredHeight();
注意,这个View的布局根节点必须是LinearLayout
//手动触发测量头布局的高度,
headerView.measure(0,0)//让系统框架去测量头布局的宽高
⑤滑出头布局
获取手指拖动的间距,设置headerView的paddingTop
=-headerViewHeight+间距
间距 = 移动的Y轴 - 按下的Y轴
额外 当paddingTop大于-headerViewHeight的时候(即间距大于0的时候),才设置paddingTop的数值
判断第一个显示的条目是否是LIstView的第一个条目.
getFirstVisiablePostion();//获取第一个可见条目的索引
如果不是,就返回super.onTouchEvent(ev)//LisrVIew默认的效果
如果是,就返回true,自己处理事件
1.2下拉刷新>>滑动中头布局状态(圆环的状态)
状态影响的通用控件:
TextView状态文本根据状态修改文本
箭头的指向(默认下拉刷新,为向下的状态)
圆环的显示
刷新状态的动画状态抽取一个方法来判断
①如果paddingTop小于0
头布局没有完全显示,显示为向下的箭头,并且当前状态为松开刷新之后,才重新进入(即反方向拖动,取消刷新)下拉刷新状态
重新进入下拉刷新状态,箭头动画效果(逆时针从-180度>>-360度,以自身为中心),
//记得当控件停止在动画播放完毕的状态
am.setFillAfter(true);
②如果paddingTop大于0
头布局完全显示,显示为向上箭头,并且当前状态为下拉刷新状态,就进入松开刷新状态.
进入松开刷新状态:改变箭头动画效果(逆时针走180度-180()向上,以自身为中心)
//记得当控件停止在动画播放完毕的状态
am.setFillAfter(true);
③松开手指的时候,
当状态为松开刷新状态,状态就为刷新中状态
箭头图片设置消失.clearAnimation()//同时清除掉自身的动画
圆环设置可见
然后再把头布显示出来:paddintTop = headerView.height
当状态为下拉刷新状态时,什么都不做,设置隐藏,paddingTop=-headerView.height.
1.3 下拉刷新(回调事件)
进入刷新中状态调用接口中的方法,这样调用者就能在这个方法里写刷新中的逻辑
创建一个方法(参数为接口对象)提供给调用者使用.
额外:进入刷新状态的时候,就不让用户继续拖动了(判断状态为刷新中,就直接跳出触摸事件)
调用这个方法,创建一个子线程,一段时间后,添加一条数据给ListView(集合中添加一条数据,更新适配器即可)
再提供一个方法给调用者,调用此方法通知ListView已经刷新完了
刷新完了,就把头部隐藏,状态更改为下拉刷新,设置圆环状态,箭头状态等.
还有最后的刷新时间.
时间格式:SimpleDateFormat = new XXXX(pattern);//pattern正则表达式
参考正则表达式:yyyy-MM-dd HH:mm:ss
Sdf.format(时间)
额外:默认最开始的时候也要设置一次更新时间
1.4 加载更多功能的实现
1.4.1 功能分析 只要用户拖到了底部,就触发加载更多的功能.
①setOnScrollListener(this)//设置滚动监听事件
重写的方法中,
//当滚动状态发生变化的时候调用
onScrollStateChanged(AbsListView view, int scrollstate)
Scrollstate>>
OnScrollStateListener.Scroll_State_IDIE;//停滞状态
Scroll_state_touch_scroll; //手指触摸在屏幕上滑动
Scroll_State_Fling;//手指快速滑动一下
②判断事件
//当前状态是停滞状态,并且屏幕上显示的最后一个条目的索引是ListVIew条目-1
就代表滑动到了底部
额外:注意监听事件的注册位置
当前状态是手指快速滑动也需要监听,因为是有惯性效果的,它不触发停滞状态.
1.4.2 加载更多的布局(加载更多只需要显示或隐藏,不用考虑拖动显示事件)
①添加脚布局this.addfooterView(view)
参考ui
②脚布局状态设置
默认状态应该为隐藏的,设置paddingTop为自己高度的负数
要注意,不能直接获取到高度,要先measure(0,0)测量一下,再获取测量的高度
当滑动到底部的时候,设置脚布局的padingTop为0即可
细节:滑动的时候不能直接滑动到底部,
setSelection(getCount())//滑动到最底部(多显示一条)
可以多次滑动到底部,触发加载更多事件,不合理,同一时间应该只能加载一次.
设置一个变量去控制它
③刷新监听器增加一个回调事件,加载更多的脚布局出现时,调用该方法.
④ListView继承类中用户调用刷新完毕的方法中添加隐藏脚布局的逻辑
设置paddingTop为-footView的高.
最后把控制变量置为默认
2.侧拉菜单(SlideMenu)功能
参考最终ui
2.1 菜单和主界面布局的实现
这是一个带有组合布局自定义控件.不适合直接继承view.
需要继承VIewGroup(View组)适合实现组合布局.
继承View的自定义控件,不需要重写onLayout()方法,因为它不包含布局
如果是继承ViewGroup的自定义控件,是必须要重写onLayout()方法.
因为它必须要有布局.有子孩子(例如LinearLayout也是继承ViewGroup的)
2.1.1 组合布局,主界面和菜单是分开的两个View
①菜单的View,是可以滚动的,所以根节点可以用ScrollView(当然也可以LinearLayout下一个ScrollView包含子节点,但是没必要,直接用它做根节点即可)
条目(可以用TextView,没必要单独写一个条目布局)带有状态选择器(pressed状态)
菜单参考ui(高度包裹父窗体,宽度固定值):
写一个颜色的xml文件(colors.xml)保存颜色.
因为每一个条目的的样式基本类似,所以可以抽取出style样式
最后在组合布局控件中引用子孩子
<include layout=”@Layout/xxxx”/>
②主界面参考ui:
ImageButton 有默认的背景颜色,可以手动指定透明颜色
ImageButton旁边有一条细线,这是一个图片,为了好看一点,让它上下有点距离,
它的右边还有一个TextView,下面的空白区域随便写点什么
最后在组合布局控件中引入子孩子
<include layout=”@Layout/xxxx”/>
ViewGroup中子孩子的顺序从上至下,由0开始.
2.2 测量和布局
2.2.1,SlideMenu组合控件继承自ViewGroup,控件的组合,是由菜单和主界面组成的.
①在onMeasure方法中测量菜单和主界面的宽高
②在onLayout方法中给菜单和主界面两个View进行布局(放置位置)
2.2.2,测量onMeasure(widthMeasureSpec,hxxx).
由于这个组合控件的宽高在布局中是填充父窗体
所以参数widthMeasureSpec(测量宽)和hxxx都是代表着填充屏幕
①测量菜单的宽和高
View menuView = getChildAt(0)//获得索引为0的子孩子(菜单)
menuView.measure(
menuView.getLayoutParams().width(通过这个View对象的布局参数获取宽度信息)),
hxxxx(如果子控件设置的包括父窗体就直接使用方法的参数)
②测量主界面的宽高
View mainView = getChildAt(1)//获取所以为1的子孩子(主界面)
Main.measure(wxxxx,hxxxx);
2.2.3,布局onLayout(boolean changed,int left,int top,int right,int botton)方法
这四个参数代表SlideMenu这个组合控件的左(0)上(0)右(宽)下(高)
①主界面的位置放到屏幕的左上角,平铺下来(宽高都设置到父控件最大)
获取mainView
mainView.layout(l,t,r,b)//设置布局位置
②菜单的位置(最初默认是在屏幕外X轴负坐标轴隐藏起来)
获取menuView控件
menuView.layout(菜单宽度取负数,0,0(与Y轴重合),b)
额外:requestWindowFeature(Window.Feature_No_Title)//代码里去掉标题
2.3 ScrollTo()和scrollBy()事件处理逻辑原理
2.3.1方法介绍
①scrollTo(int x,int y)
给定固定的偏移量,屏幕会显示到对应的位置上
(从最开始的起点为基点,而不是上一次移动的点,最开始起点一般为屏幕左上角0.0)
②scrollBy(int x,int y)
给定移动的值,会把屏幕原来左上角的X轴左边值取出来加上给定的值(即在每一次移动后的基础上移动)计算新的值,移动到对应的位置.
2.4,touch触摸事件的处理
按下:记录下X轴的值,downX;
移动:记录下移动X轴的值,moveX
计算增量值 = downX-moveX(因为屏幕移动和控件移动的显示是相反)
使用ScrollBy(增量值,y)//移动到对应的位置
抬起:
①获取按下的值和移动的值,得到增量值(down-move,理论上绝对值固定为1?(已解决,并不是固定为1,moveX的值是根据单位时间内获取一次,而不是一个像素点一次))
Scrollby(增量值,0);//滚动到相应的位置
移动的值重新赋值给按下的值
额外1:边界会不合理的超出(不符合用户的预期)
解决1:判断移动的值是否会超出边界
getScrollby()+增量值//获取当前已经移动的值+增量值,是否超出边界
左边界不能超出菜单栏的左边界(<=菜单栏的宽度取负数)
右边界不能超出主界面的右边界(<=主界面的宽度)
优化考虑:如果超出了,直接return掉,是否效率更高?,不用再调用方法滚动.
②松开的时候,判断菜单是否需要显示在可视界面上.
这里以菜单栏宽度的一半(取负数)为标准,与移动的值做比较
如果移动的值大于菜单栏宽度的一半,就切换到主界面
如果移动的值小于菜单栏宽度的一半,就切换到菜单栏
抽取一个方法,根据不同的情况,设置移动的值ScrollTo(xx,0);
额外1:松开的时候,没有一个滚动的效果,而是直接跳过去了
解决1:可以自己模拟数据,来实现滚动的效果,但是太麻烦(计算值,每秒移动值等)
android中提供了一个Scroller类来实现该数据模拟
代码实现步骤:
scroller.startScroller(sx,sy,dx,dy,duration)//模拟滚动的效果
sx:开始的位置,dx,结束的位置
duration:持续时间
①分析各个参数具体的值
开始的位置:最后一次移动后屏幕的点,sx = getScrollX();
结束的位置:增量值,目的地的值-开始位置的值
持续时间应该是动态的,不然移动位置太短,时间就会显示很长,干脆动态的设置为增量值*10毫秒
②scroller.startScroller(x,x,x,x)//该方法只是去模拟值,但是不负责显示设置的值
所以还需要自己去移动切换屏幕
用一个while循环,当数据在模拟的时候,不停的取值切换屏幕
不过,谷歌已经提供了方法来实现这个功能
Invalidate();//刷新当前控件,不断调用onDraw()方法
但是ViewGroup父类中是没有onDraw()方法的.不过它有drawChild(xxx)方法,绘制子控件,所以继承它的组合控件也会去调用drawChild(XXX)方法
③查看源码可知:
drawChild()>>return child.draw(xxxx)//调用每一个子控件的draw方法
调用的是view.draw(xxxx)方法(三个参数的,直接跳过去看到的是一个参数的)
>>这个方法里可以看到调用了一个view.computeScroll()方法,注释翻译:调用在父类去请求更新(可以覆盖掉)scrollX,scrollY的值,然后进行移动的操作
那么在这里就可以去模拟一些数据去更新这两个x,y的值
在computeScroll()方法的注释上也可以看到谷歌是建议使用Scroller模拟数据
④重写view.computeScroll()方法,取出模拟的数值
int currX = scroller.getCurrX()//取出正在模拟的数值
scrollTo(currX,0);//把对应的数值传递过去
调用一次invalidate()方法,只触发一次computeScroll()//方法
所以在computeScroll()方法中再调用invalidate()方法
类似与递归,需要找到一个出口,让这个递归停下来
如果数据模拟完毕,就不再进行递归调用了
scroller.computeScrollOffset()//返回为true 代表这个动画(数据模拟)还未完成.
2.5 点击切换屏幕
①点击ImageButton 切换屏幕显示
实际上就是切换这个自定义组合控件在屏幕上显示的位置
判断当前显示的状态
如果显示的是菜单界面,就切换到主界面完全显示
如果显示的是主界面,就切换到菜单界面的显示.
切换显示的效果可以用上面实现的界面移动逻辑(优化时间显示)
②菜单栏里每一个小条目的点击事件,点击完之后都会隐藏菜单栏,完全显示主界面.这里可以把点击事件写在样式里,这样每一个条目都有对应的效果了.
2.6 事件分发机制
问题描述:设置完点击事件了,在菜单栏中无法拖动,一旦拖动,走的都是点击事件,而不是预期中的切换屏幕显示,是由于事件分发机制引起的问题.
2.6.1 事件分发机制的原理
①方法
每一个ViewGroup都有下面的方法
dispatchTouchEvent()//分发事件用的方法
onInterceptTouchEvent();//拦截事件用的方法
onTouchEvent();//处理事件用的方法
每一个View都有下面的方法
dispatchTouchEvent()//分发事件用的方法
onTouchEvent();//处理事件用的方法
②当一个事件开始了,会走最顶层的ViewGroup(父控件)的事件分发>>
>>拦截事件 判断拦截事件的返回值
返回true 拦截这个事件,就传递给自己的onTouchEvent()处理事件,事件终止
返回false 就不需要处理,传递给下一层,判断是否拦截
事件一直到传递到最下面的子控件view,它是不包含子控件的,也就没有拦截事件的方法去判断是否拦截,直接走view的onTouchEvnet()
返回为true,处理事件,事件终止
返回为flase,不处理时事件,向上回传
向上回传,上一级ViewGroup的onTouchEvent是否处理,同样的继续回传或处理.
事件一直到最上层的父控件onTouchEvent方法中
如果返回为true处理当前事件
如果返回为false不处理当前事件,事件直接消失掉,
参考流程图如下
2.6.2代码实现
在自定义组合控件代码中拦截事件onInerceptTouchEvent(event)
//判断是否是横着滑动
就是按下与松开的位置X轴之差(绝对值)大于某一个值就代表是横着滑动的,拦截掉这个事件,比如大于10的时候就返回一个true,拦截掉这个事件
这个值为10在某些屏幕手机上使用可能不太合理.
所以使用google提供的VIewConfiguration.get(getContext()).getTouchSlop()它返回的值是根据不同手机屏幕返回的,用它来做滑动事件判断标准比较合理
注意:事件分发在面试的时候问的比较多,要多理解掌握
3其它补充
自定义属性:使用
在一个布局文件根节点中 xmlns属性(xml属性的命名空间)
可以指定多个xmlns指定不同的命名空间
xmlns:xxxx(自定义名称)="http://schemas.android.com/apk/自定义名称(res-auto参考)
创建 res下values创建根节点为resources的xml文件
子节点declare-styleable name=”一般为使用它的文件名”
这个节点下的子节点
attr节点 format属性,可以指定自己想要的属性,指定frxxx就可以使用资源文件
在自定义控件的布局构造中(两个参数的,attrs方法)
attrs.getXXX可以获得布局文件中的参数