自定义控件进阶
1,侧滑面板(类似侧拉菜单,但是更加强大,动画更多)
应用场景:QQ,知乎,贴吧,给主界面添加更多功能
1.1 界面初始化分析,完成界面如图(左面板最初是被隐藏的,用户在屏幕上滑动,左面板由小放大进入屏幕,右面版由大缩小移出屏幕)
由图可知,这个侧滑面板和主界面实际上是一体的,但是这里不继承ViewGroup,继承FrameLayout.
因为继承ViewGroup需要自己去测量onMesure(xxxx)和布局onLayout(xxx)
而FrameLayout它自己重写了这两个方法,会测量子控件,并且它不像LinearLayout固定死了控件方向,只有层级关系,布局更加灵活,所以组合控件推荐继承FrameLayout
1.2布局文件
根节点为自定义控件,因为它是继承FrameLayout的,所以可以按FrameLayout进行布局
先做底层的布局(会被覆盖的布局)再做上层的布局
分析:左面板和右面版都适合用LinerLayout进行布局(为了布局方便,给这两个面板加一个背景,确定它们的布局位置)
额外1:在控件中有translationX 可以让控件位移一段距离,对不同的布局设置不同的值,可以在设计时看到不同的分层(区分覆盖层和被覆盖层,还有底部)
1.3.1 ViewDragHelper//android support-v4包的
视图拖拽辅助类,2013年google I/O提出,解决控件在界面的拖拽问题.
关联源码的方式②,在lib下创建一个跟jar包同名的(包含后缀).properties文件
里面写src = 源码的路径//硬盘上的路径,单斜线要改为双斜线,window文件系统需要.
然后close掉app重新打开
//构造被私有,用create方法获取实例
①创建ViewDragHelper辅助类
ViewDragHelper mHelper
=ViewDragHelper.create(ViewGroup parent,float sensitivity,Callback cb)
参数:parent:所有需要被拖拽的View的父容器,这里可以直接写this
sensitivity:敏感度,检测拖拽反应率,数值越高,越容易触发事件1.0为标准敏感度
cb:事件的回调,抽成成员变量直接new出来即可
②串联三个构造函数
在别的构造函数中,this(xx,xx,xx)//一个的串两个,两个参数的串三个参数函数(因为前面的辅助类创建在三个参数的构造函数中,所以这样串起来)
③转交触摸事件
onInterceptTouchEvent(),在拦截方法里,返回值为mHelper.showldInterceptTouchEvent(ev)
由辅助类对象判断事件是否该拦截
onTouchEvent方法中,最好是直接返回true,把事件转交给mHelper对象处理
mHelper.processTouchEvent(ev)//抓个异常,多点触摸会有小异常,抓一下
④接收处理的结果
在自定义控件类中
onFinishInflate()中,当所有控件显示到界面之后调用,可以找到子孩子
getChildAt(XXX);//根据索引获取孩子
但是为了保证代码的健壮性,孩子必须有两个,
否则抛异常:throw new IllegalStateException(String xxxx)
孩子必须是ViewGroup的子类() instanceof ViewGroup
因为这两个子控件都还要有内容
否则抛异常:
Throw new IllegalArgumentException(String xxxxx);
1.3.2 在回调中重写的方法里,返回值决定了child是否可以被拖拽
①tryCaptureView()
参数child被用户拖拽的孩子,pointerId多点触摸手指的id
还需要再重写的方法
②向父类返回水平拖拽范围,返回一个>0的值,决定动画执行的时长和水平方向是否可以被滑动
getViewHorizontalDragRange(View child)
它在ViewDragHelper中的使用
computeSettleDuration(动画执行时长的方法,通过这个方法返回值计算时间)
checkTouchSlop()(通过判断拖拽范围的符合条件,如果这个方法返回值为<=0,在检查是否符合范围的时候就为false0,不处理,不过当子孩子没有处理的时候,向上回传父控件依然会处理,因为向下传递会拦截,向上回传并没有拦截)
要动态的设置返回值,在自定义控件类中重写onSizeChanged(w,h,,oldw,oldh)方法,它是在onMeXXX()测量之后,尺寸发生变化后调用(直接通过onMesXXX()方法也可以,但是效率低).
//获取宽高,
在这个方法里getMeasuredHeight(),getMeasuredWidth()
那么拖拽范围的返回值推荐修正为mRange = mWidth*0.6f
同样还有返回垂直拖拽范围的方法,原理类似
③/修正子View水平方向的位置,还没有真正的发生移动
clampViewPositionHxxx(child,int left,int dx)
//返回0,代表永远保持原样,返回固定值,一旦拖拽,就按这个值进行移动
参数left建议移动的位置,如果接纳这个值,就返回它(一般都是拖拽的点位)
dx跟旧的位置的差值(单位时间内的移动至与旧的值的差值)
clampViewPositionHxxx(child,int left,int dx)//修正View竖直方向的位置
如果不重写,父类是一直返回0的,不会竖直移动
④当位置发生变化的时候调用,伴随动画,状态的更新,事件的回调
onViewPositionChanged(View,left,top,dx,dy)
Left,top,最新的位置,dx,dy,刚刚发生的变化量
观察可知,拖动的时候,主界面的左上角小图标,移动时候的动画效果都是在这里实现的.
侧滑面板,左面板实际上一直不动的,动的只有右面板
所以当用户移动的是左面板时,
放回原来的位置,位置不变leftMianBan.layout(0,0,0+屏幕宽,0+屏幕高)
同时把数值传递给右面板
//获取右边板的newleft = left(右面版.getX()) + 变化量dx
Right.layout(newleft,0,newleft +屏幕宽 ,0 + 屏幕高)
//同时也要进行修正
所以在把上面的修正方法抽取出来,这里可以再用一次
1.3.3低版本拖拽问题
低版本拖拽不生效
底层在真正实现拖拽效果的方法是View.offsetLeftAndRight(in offset)
高版本的这个方法里,代码量更大,并且调用了一个关键方法invalidateViewProperty()刷新了视图属性,而在低版本中,这个方法里代码很简单,并且没有刷新视图属性
解决方法
直接在拖拽完之后invalidate()//兼容低版本,手动重绘界面所有内容
同样,在拖拽的时候如果有缝隙,通过invalidate()可以让最后一次拖动生效,这是一个小bug,最后一次拖动可能不会生效
1.4 松手动画的处理(这里松手动画时指在屏幕上滑动一下,然后触发打开或关闭界面的事件)
①在回调对象中重写方法,决定了松手之后要做的事情,结束的动画
onViewRelesed(View,xvel,yvel)
View:被释放的子孩子
xvel水平方向的速度(向右为正,向左为负)
yvel竖直方向的速度
判断当加速度等于0,获取右面板的的位置,如果左边距大于拖动范围的一半,打开一个面板
如果水平方向加速度大于0,也是打开一个面板(打开右面版)
其它情况下就关闭面板(设置最终left等于0即可)
打开面板,最终finalLeft = mRange
1.4.2平滑动画处理,ViewDrawHelper封装了一个scroller对象可以做平滑动画
①如果在平滑的情况下,执行平滑的动画
重载一个open()的方法,参数为一个布尔类型的变量,判断是否是平滑状态
在原来的open()方法里调用这个重载的方法即可
在重载的oepn()方法里
②如果是平滑状态,触发一个平滑动画
mHelper.smoothSlideViewTo(右面板,finaleft,0),返回一个布尔值,当前是否需要执行动画,即当前是否需要开启动画,返回true,需要重绘界面,返回false(当前右面版已经完整显示了)
重绘界面,不建议用invalidate()刷新数据,因为可能会漏帧,当多个invalidate()同时执行的时候,短时间内可能只会执行一次这个方法.
使用ViewCopat.postInvalidateOnAnimation(view)//一定要传this,子view所在的容器
③维持动画的继续执行,会高频率的调用
computeScroll(),这个方法在绘制动画之前执行.在这个方法里:
如果需要画下一帧mHelper.continueSetting(true)//返回一个boolean值,true代表还有东西要画,google提示如果这个方法在computScroll()中就需要传入true
然后需要再次ViewCopat.postInvalidateOnAnimation(view)刷新数据
同样,close()方法关闭界面也是一样的
1.5 伴随动画
①分析:四个模块,小图标的隐藏与现实,左右面板随着滑动而放大与缩小,背景亮度的变化
左面板:缩放动画,平移动画,透明度动画
右面版:缩放动画
背 景:亮度变化(颜色动画)
图标:透明度动画
②:在onViewPositionChanged(View,left,top,dx,dy)中做动画的分发操作
参考方法名:dispatchDragEvent()
//0.0 -> 1.0动画播放的时间轴
得到这个速率,观察可知,时间轴float percent = 右面版的左边界*1.0f/mRange*1.0f;
实现缩放动画
面板.setScaleX(倍率(最小也得有0.5美观))(percent*0.5 + 0.5就可以得到)
Jake Wharton写的一个框架
-nineoldandroids兼容低版本安卓,让属性动画可以生效
viewHelper.setScaleX(需要播放动画的子控件,数值).
主面板的缩放动画从1.0->0.8
实现平移动画
viewHelper.setTransLationX(子控件,数值);
在typeEvaluator类型估值器中有一个方法
evalute(倍率,开始值,结束值),可以返回一个中间值,就不用自己算了,上面的缩放数值也可以这样写 (要把类型估值器的方法copy过来)
实现透明动画
viewHelper.setAlpha(控件,数值)//同样也可以用类型估值器的方法算出来
背景亮度变化,设置背景颜色过滤器
getBackground().setColorFilter(Color颜色,mode(PorterDuff.Mode.SRC_OVER))
模型这里,可以参照V4apiDemo里面的实例来设置
相当于把一个颜色覆盖在了背景上面
继续找类型过渡器(api18以上,18以下有时候不能过滤透明度)
Evalute(float,开始值,结束值)//颜色过渡器
需求是从黑色,过滤到透明色(Color.TRANSPARENT)
1.6 数据填充
自定义控件界面数据填充的时候,不要用margin,用padding设置数据离边界的距离
当然子控件就无所谓了.
右面板 左面板
这些列表都是ListView,主面板有个点击事件,小图标,其中小图标还会做抖动操作.
用数组适配器的话,指定的layout条目样式文件可为系统默认的
android.R.layout.simple_list_item_1(当然也可以自己写)
设置数组适配器,也可以new ArrayAdapter(){里面重写方法,不过数组适配器不强制重写}
1.7 触摸优化
①三种状态:滑动状态,菜单完全显示状态,菜单关闭状态
用枚举写这三种状态
public static enum State{
Close,Open,Draging
}
还需要一个对外的事件监听器(三个强制方法,关闭,显示,滑动状态(移动比率))
提供一个方法,让调用者设置拖拉监听器,新建对象
更新状态应该在伴随动画,设置动画播放比率的方法内使用
根据percent比率来判断,如果是0就是关闭状态,如果是1,就是打开状态,其它就是滑动状态
对外的事件监听器,回调的方法应该在状态改变的时候调用(不是每一次获取状态,太频繁了,获取前一次状态,更新完状态再比较一次,然后根据不同的情况设置回调方法)
最后在调用者类中实现监听器.
②单例的吐司
创建一个工具类,提供一个showToast(上下文,String文本)方法
Toast = Toast.makeText(上下文,””,时长);//判断toast是否为空,为空通过这个方法获取
然后设置Text,最后Toast.show();
③调用者类中效果的实现
在Open()回调方法中,让左边的ListView随机跳到某个位置
leftListView.smoothScrollToPosition(随机数)//平滑的滚动
在拖拽的回调方法中,修改小图标的透明度
在Close()回调方法中,让右边的ListView随机跳到某个位置
让小图标晃动(参考apiDemo里面的输入框为空的移动效果)
也可以用属性动画
ObjectAnimatior animor = ObjectAnimatior.ofFloat(控件,属性(这里为平移),数值)
animor.setInterpolator(new CycleInterpolator(次数))
④点击事件,点击小图标打开界面
1.7.2,触摸优化的实现
当左面板全部显示的时候,右面版的ListView不应该能被滑动
自己写一个自定义线性布局,专门用来拦截ListView的滑动事件
拦截方法onInterceptTouchEvent(ev);
判断是否是关闭的状态,如果获取当前状态,
在主界面中找到这个自定义线性布局,然后直接把这个dragLayout传递进来.
再进行判断即可(记得判断是否为空)
然后重写onTouchEvent(ev)中,也做同样的判断
同时在这个线性局部范围里松开了,也让左面板关闭掉
2, 快速索引(自定义view)
应用场景:联系人,微信好友列表,应用管理,文件管理(视频,音频文件)
2.1 步骤分析
绘制静态的A-Z字母列表
响应触摸事件
提供监听回调
获取汉字的拼音,得到首字母
根据拼音排序
分组归类
把监听回调和ListView结合起来
参考结果图:
2.2 静态绘制
快速索引栏(就是右侧的索引条),自定义类,继承View
画文本:drawText(“文本”,left,top,画笔);
这里需要一个画笔了new Paint(Paint.ANTI_ALIAS_FLAG);
letterPaint.setTypeface(Typeface.DEFAULT_BOLD);//设置粗体
因为需要二十六个字母(还有#*),可以写成一个String数组,遍历这个数组,不断画文本
top距离顶部的距离,根据索引位置*间隔值,就能动态的得到
在onMeasure()中获取宽高信息
cellWdith就是控件的宽,cellHeight就是控件的高除以绘制的个数
在onDraw()里开始画文本
宽度可能除不尽,所以用float,计算的时候int数乘以1.0f转换一下
除以2最好是转换成2*0.5f.
画笔也可以测量文本,paint.measureText(文本)*0.5f
所以宽度x就是 cellWidth*0.5f-paint.measureText(“文本”)*0.5f;
获取文本的边线矩阵
Rect Bounds = new Rect()//矩形
paint.getTextBounds(文本,0,,文本.length(),bounds);
所以高度y就是cellHeight*0.5 + bounds.height*0.5 + i(索引)*cellHeight
2.3 响应触摸事件
onTouchEvent();//因为在控件上的事件都是自己处理,所以返回为true
在按下的事件中,获取到当前对应的字母索引(按下的高度除以单元格的高度)
移动的时候也做同样的判断(因为需要在移动的时候获取索引才能调节ListVIew内容)
额外细节:横向移动的时候不应该重复获取索引,所以,记录老的索引与新的索引做比较
松开时候把老的索引重置为-1
2.4 回调事件
获取回调的位置:在获取到新的有效索引时进行回调
2.5 根据拼音排序
①第三方jar包,拼音4j
通过中文字符串获取拼音
用法:
Char//通过遍历中文字符串获取
HangyuPinyinOutputFormat format = new xxxx();//格式化对象
PinyinHelper.toHanyuPingyinStringArray(char,format)//返回一个String数组
为什么是数组,因为有多音字啊
用一个StringBuilder接收到所有的拼音(方便排序)
优化①:获取字符char的时候,判断一下Character.isWhitespace(c)//如果是空格就跳过
判断是否是码表中的值(ascll码表的值)-128~127就不再获取拼音,而是直接添加到sb
②这样获取之后发现居然会有声调
//不要声调
format.setToneType(HanyuPinyinTonType.WITHOUT_TONE)
//设置转换出大写
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
②填充ListView数组
这一次不用匿名内部类了,创建一个新的类继承BaseAdapter()
在继承类中,创建一个集合储存数据
这个集合封装了拼音的对象,创建一个拼音的domian实体类,两个属性,姓名和拼音
其实拼音不用传进来,直接在构造中传入姓名,然后通过工具类获取拼音
注意:在继承类中,构造让它传入集合和上下文,方便进行数据处理
集合中数据排序,Collections.sort(集合(集合中的元素需要实现排序接口))
然后在实现的方法里,按字符串进行返回值判断
分类的索引条目,让它和每一个子分类的第一个条目进行显示即可
(默认每一个子条目都是带标题栏的)
判断当前首字母和上一个首字母是否一致,如果一致,就让标题栏的TextView消失,
如果不一致,就代表这一个条目是按拼音分类的第一个条目,显示标题栏
根据数据组织界面是自定义控件的核心内容之一
2.6 监听和ListView结合起来
根据快速索引栏的回调中获得的字母与集合中每一个元素首字母进行匹配,如果匹配了就break掉(因为排序过,一旦匹配上就相当于是到了带标题栏的第一个条目)
然后ListView.setSelection(对应索引即可).