自定义控件基础02_下拉刷新_侧拉菜单_自定义属性

自定义控件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)//让系统框架去测量头布局的宽高

 

⑤滑出头布局

获取手指拖动的间距,设置headerViewpaddingTop

=-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)测量一下,再获取测量的高度

当滑动到底部的时候,设置脚布局的padingTop0即可

 

细节:滑动的时候不能直接滑动到底部,

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,它是不包含子控件的,也就没有拦截事件的方法去判断是否拦截,直接走viewonTouchEvnet()

返回为true,处理事件,事件终止

返回为flase,不处理时事件,向上回传

 

向上回传,上一级ViewGrouponTouchEvent是否处理,同样的继续回传或处理.

事件一直到最上层的父控件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参考)

 

创建 resvalues创建根节点为resourcesxml文件

子节点declare-styleable name=”一般为使用它的文件名”

这个节点下的子节点

attr节点 format属性,可以指定自己想要的属性,指定frxxx就可以使用资源文件

 

在自定义控件的布局构造中(两个参数的,attrs方法)

attrs.getXXX可以获得布局文件中的参数

 

posted on 2016-07-04 23:16 抓根宝 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/adventurer/p/5642029.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值