zoukankan      html  css  js  c++  java
  • 自定义控件基础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可以获得布局文件中的参数

  • 相关阅读:
    【逻辑漏洞技巧拓展】————4、逻辑漏洞之支付漏洞
    【逻辑漏洞技巧拓展】————3、逻辑漏洞之密码重置
    【逻辑漏洞技巧拓展】————2、Web安全测试中常见逻辑漏洞解析(实战篇)
    【逻辑漏洞技巧拓展】————1、逻辑至上之各种酷炫姿势
    【文件上传/解析技巧拓展】————4、文件上传总结
    【文件上传/解析技巧拓展】————3、服务器解析漏洞
    【文件上传/解析技巧拓展】————2、文件上传漏洞(绕过姿势)
    window 杀进程
    URL的三部分
    基本的浏览器连接处理步骤
  • 原文地址:https://www.cnblogs.com/adventurer/p/5642029.html
Copyright © 2011-2022 走看看