Tab 模块 (四)

134 篇文章 0 订阅
87 篇文章 1 订阅
Tab 模块 (四)


TabViewListView是真正负责显示TabView,以及相关TabView交互的View,
extends AdapterView<TabViewListAdapter>, 使用AdapterView而不是更为现成的ListView的原因是,
ListView虽然在展现List方面很方便很现成,但是ListView不支持对某个ListItem的Touch复杂交互<drag检测,当然还有自己的私心,希望试试中高阶>,
要在ListView的基础上实现Touch交互也不是不可以,但是ListView本身成熟的实现反而成了障碍,会使拓展变得难以进行.
因此就祭出了AdapterView<直接extends自 abstract ViewGroup>,很多开源项目的自定义View都会采用AdapterView作为base,而不是ViewGroup.
使用AdapterView的主要代价就是 measure和layout这部分实现都要自己来搞,不过因为只是一个list,所以不是很复杂,
而AdpaterView的notifiyDataChanged()不是和ListView一样已经自动挂接好了刷新UI的逻辑,需要自己挂接<DataSetObserver>.


<1>为了实现notifiyDataChanged()触发UI刷新,DataSetObserver必不可少,
需要override的是两个函数:
onChanged(): 代表着View对应的数据<M>变化了,要做的操作,在当前的实现中就是重新根据M构造好相应的TabView并加入<addViewInLayout()>,然后requestLayout
onInvalidated(): 代表着View的图像内容 invalid了,简单调用invalidate重绘即可.
实例化完对象以后需要将其register到Adapter中,才能在notifiychange的时候被通知.
注意,AdapterView的角色定位<注释有写>和ViewGroup不一样,ViewGroup可以允许外界addView到自己,但是AdapterView是不允许外界直接AddView的,
因为AdapterView的幕后是Adapter,对AdapterView的所有修改都应该通过Adapter<或者说,AdapterView只负责呈现Adapter规定的内容>,不允许直接修改。
因此在AdapterView中不能直接add/removeView(), 而只能add/removeViewInLayout().


<2>和MovingMenu一样,对于Touch的处理,使用了GestureDetector并将Touch相应处理逻辑和信息封装在一个TouchController类中.
TabViewListView处理的TouchCase有两种:
(1)TabViewListView 本身的上下滚动
(2)某个TabView被drag.


(1)为了知道某一次Touch事务中,被drag的TabView<被点到的TabView>是哪个,需要保存一个draggedViewId,以及一个指向该TabView的引用.
(2)前面说过,在drag tabView的时候,被drag的View其实不是TabViewListView中的那个View,而是一个上一层UI中的一个辅助View,只不过看起来和被drag的一样,
那么这个View的引用也要被保存以便在一连串的事件中都方便得到. 因为辅助View是通过setBackGroudResouce为被drag的Tabview的截图实现的,
因此还要维护一个指向截图的bitmap.
(3)在dragg的过程中,会有渐隐效果,通过调整View的alpha实现的(1.0不透明-> 0.0X基本消失)
(4)down的MotionEvent坐标, 开始drag时的MotionEvent坐标, 不直接保存MotionEvent,MotionEvent是种池化资源,直接保存引用会有问题,而通过obtain复制只为保存X/Y则显的浪费.
(5)被drag的TabView的起始X/Y
(6)drag中,当前被drag到的位置的X/Y
(7)一个threshold,velocity超过此值的fling操作,被视为删除一个Tab<包含M中的Tab和V上的TabView>
(8)TabMenu在整个Screen的坐标<getLocationOnScreen>
(9)和之前一样,也会有mDown和mDragging flag,用作检测和控制是否该移动View.
(10)还会保存当前支持的drag<vertical/horizontal>
(10)还会保存当前支持的drag<vertical/horizontal>


<3>因为TabView本身在设计中不会产生交互<可以被drag是由TabViewListView实现的>,所以TabViewListView的onInterceptTouchEvent不需要再被override
<不会存在子View消化了TouchEvent>,直接override其onTouchEvent就可以.


<4>同样GestureDetector会禁用LongPress检测.


<5>onTouchEvent在开始要做isEnabled判断,如果disable,直接return false回吐.
ACTION_CANCEL要列入考虑范围<被parent Intercept会导致, 当然也会被其他原因触发>, ACTION_CANCEL可某种意义可以视为本次Touch事务的结束,
而对TabViewListView来所,如果一个Touch事务结束,要做的事情有三个
(1) 将当前被drag<如果有>的View归位.
(2) 将被drag的TabView恢复到 unSeleced.
(3) stopDrag 将 hasDown和onDragging reset, 表示一次dragging的结束
否则将MotionEvent扔给GestureDetector处理.
最后,再判断是不是Action_UP,如果是,也代表一次Touch事务的结束,将被drag的TabView设为unSelected, 并将hasDown和onDragging reset.
如果发现本次的ACTION_UP没有被GestureDetector处理,那么会再自己判断一次是不是我们认可的fling<使用rawX/Y>, 还有本View也考虑了OverScroll的情况,
如果OverScroll了,那么在UP的时候还要将OverScroll恢复.
在都没有被处理的情况下,回吐。


<6>TouchController也需要检测orentation的变化<不同orentation的可drag和拖动的方向不同>,这是通过TabViewListView在被告知orentation改变时被通知的,
每次变化,都会停止当前的drag.


<7>在GestureDetector的SimpleOnGestureListener的自定义类中,onDown和之前的MovingMenu的实现不一样,不是简单的转交给TouchController,
之前需要做一些是否点中的检测.
同MovingMenu一样,对onScroll提供的distanceX/Y不信任,自己保存上一次Move的endX/Y来同现在的作对比得到.
这里虽然之前说过用一个MotionEvent保存X/Y比较浪费,不过当时想试试MotionEvent的池化回收<recycle>和轻量复制<obtainNoHistory>,
就用了一个MotionEvent的引用指向保存了上次down/scroll结束的位置的MotionEvent.
在SimpleOnGestureListener的onDown中,先常规的检测传入的MotionEvent是否为null, 然后将指向上一次的MotionEvent释放<recycle,只将引用改变不会使此MotionEvent被回收,当然这是在
引用指向了某个MotionEvent对象的前提下>, 在down情况下,先做个判断,看是不是down到了某个TabView上:
这里要注意一点,因为实现List的上下滑动是通过scrollTo,那么在做Touch判断的时候,肉眼看down到了 (x, y), 其实是down到了 (x-scrollX, y-ScrollY),
所以在做是否X,Y在某个范围内时,要将MotionEvent<不使用RawX/Y,因为都是在同一个View的,相对坐标足矣> + scrollX/Y.
然后遍历childView, 判断(x,y)是否在child的范围内(child.getLeft, child.getTop, child.getLeft+child.getMeasuredWidth(), child.getTop() + child.getMeasuredHeight())
<用MeasuredWidth/Height的原因是因为在AdapterView里,一切Measure Layout都是自己的实现,因此在本设计中,ChildView的MeasuredWidth/Height就是ChildView layout以后的
真实尺寸>,获取了落在childView的id以后,返回Id。
如果Id也是有效的<保险>,那么就可以将此Id和Id对应的TabView和本次Touch事务以及TouchController绑定在一起了<作为参数传入了TouchController的down函数>,
因为设计中还要求了如果List正在自动的滑动<scroll>,那么如果有Touch的话,要停止自动的滑动.
后面的细节先略过.


<8>在onScroll的处理中,
之前有一个MotionEvent保存了上一次down/Scroll结束的EndX/Y,和现在的e2<当前scroll的EndX/Y>做一次比较获得移动的distanceX/Y<出于保险,要考虑到上一次MotionEvent为null的情况,
这种情况,就使用e1作为startX/Y来得到distanceX/Y>,然后保存这次的e2<obtainNoHistory(e2)>,扔给TouchController处理.
如果发现TouchController没有处理,这时候有可能是List已经滑动到头,在后面要尝试OverScroll.


onSingleTapUp的处理简单直接转交TouchController.
TouchController的singleTap会先判断是否mHasDown和!onDragging,
然后判断是否点击到了TabView上代表关闭的View,如果不是,那么认为是选中了某个Tab作为当前Tab,会讲此Tab设为Active,并且告知M这个变化<通过mTabContainer送出>,
然后这种情况也会导致List的自动滑动停止.
如果是关闭某个TabView,那么会先将这个List disable, 然后remove. 在remove完以后才会 Enable


<9>删除最后一个Tab和普通的删除一个Tab有不同,
普通删除Tab会将View remove<只能removeViewInLayout,而不能removeView, 这个可以选择是否显示动画效果>,然后通知Adapter Remove, 最后通过通告TabContainer来将Tab被删除的消息告知M层.
删除最后一个Tab将会导致一个新的默认Tab被添加到List中,这个Tab被删除的消息也要告知M,但是后面的操作会被暂时的PostDelay.
有动画效果的Remove会比较复杂,首先辅助View会被VISIBLE<上场了>,被替代的TabView会被post下一个slice INVISIBLE<为了提升Remove的流畅度,效果可以接受>,
然后根据当前orentation的不同,动画的方向也会不同,
动画的效果其实也是模拟真实的手势拖动,然后拖到某个位置以后,View消失,List重新刷新,这样可以直接复用某些函数.
这里的drag和之前MovingMenu的drag也比较相似,会对第一次的drag<scroll>做一些特殊处理,标志着drag的持续开始,也会设置和使用dragging flag,
传入的X/Y作为当前的坐标,计算出从down到现在位置的位移 distanceX/Y, 如果在X/Y轴的drag, 移动的distanceY/X 反而比 distanceX/Y大的话,视为一次无效的drag,直接stop drag,并返回false.
否则设置辅助和被drag的View的VISIBLITY,并且更新startDragX/Y为当前的X/Y, 如果不是第一次的drag,那么直接用现在的X/Y和之前保存的上一次的endX/Y, 就可以得出手指移动的距离,
然后让辅助View移动相同的距离就可以. 


<10>移动辅助View的操作也被独立封装为一套函数,并且会做mHasDown和onDragging的判断,这一步做的是根据moveX/Y得到当前被drag的辅助View应该的坐标X/Y,
然后调用一个专门的函数来根据moveX/Y更新辅助View的位置和alpha. 
这里移动辅助View不是通过scroll了,而是通过调整辅助View的Margin<leftMargin 和 topMargin>实现的
<getLayoutParams/setLayoutParams>. drag的时候还会调整辅助View的alpha.
要注意的一点是,辅助View和List并不属于一层UI<所以辅助View才能移动到List外边>,而当前的MotionEvent的X/Y都是以List为原点的,因此还需要考虑到List的原点的位置<getLocationOnScreen>
,最后得到的才是适合辅助View这一层的Margin.
为了方便使用,移动辅助View这个操作仿效scroll被实现为了两种方式, to和by, 只是坐标计算稍微变一下.


<11>对down的处理,会触发对辅助View的初始化<如果没有init的话>, 因为辅助View不在ListView这一层,因此还需要通过getRootView的findViewById才能找到,
初始化主要就是给辅助View设置合适的FrameLayout.LayoutParams,还会将被down的TabView和Id保存在TouchController,标志着这一次的Touch事务都以此操作.
<出于鲁棒性,会检测一下传入的TabView是不是null>,然后再stop一下drag<虽然这个down可能是被onDown调用,而已经stopDrag了,但是因为是分离为不同的函数,
因此每个函数,出于对独立性的考虑,都会独立的stopDrag一下>, 然后重新设置mHasDown = true 和 mOnDragging = false, 
TouchController的mDownX/Y 也会保存成这次 Touch_down的RawX/Y, 同时为了下一步可能的dragging, 会算出辅助View应该的坐标<要考虑到不同UI层>,
做一次hitest看是否点到了close View, 如果是,那么单独将close View设为selected,否则,整个TabView设为Selected<只是down到close View
不能关闭,必须有一次完成的singleTap才算可以close>,这里为了截图,使用了setDrawingCacheEnabled和getDrawingCache,注意点是,setDrawingCacheEnabled在true之后用完要
回到false,而得到的getDrawingCache必须通过Bitmap.createBitmap复制一份独立的才能使用,然后调用辅助View的setImageBitmap就可以将辅助View伪装为被点击的View了.
然后辅助View为了容纳TabView的截图,还需要通过LayoutParams对其width和height调整为截图的Width和Height<昨晚这些以后没有setLayoutParams促使UI刷新,因为后面放置
辅助View的时候还会调整,那时候才会setLayoutParams>


<12>对于fling的处理,通过自己保存的DownRawX/Y和fling 松开时的 RawX/Y, 得到移动的MoveX/Y,根据是vertical/Horizontal的dragMode,
判断相应的Move距离是否超过threshold或者velocity超过threshold, 如果是,那么就开始Remove TabView的动画,并且最后M会获知M被删除.并且消化此Touch事务.
否则,就将当前被移动的辅助View归位,也会触发动画<封装为了函数>.
fling的检测和处理函数在Action_UP,没有被GestureDetector处理的情况下也会被调用,这是为了实现 drag View离开一定距离时,松开,View也会自动Remove或者归位.
在fling中的MoveX/Y检测就是drag这种操作而加的.


<13>ListView的交互复杂在于既要处理tabView被drag<本身已经挺复杂了>,还要处理作为一个ListView本身可以上下/左右拖动的功能,
在设计时,前者处理的优先级最高,只有在前者的交互逻辑handle不了时,才会交给后者尝试处理.
List的滑动本身的实现简单,scrollTo/By即可,但是还要考虑Touch交互以及fling造成的自主滑动和overScroll.


<14>在onScroll处理逻辑的最后,如果scroll event没有被GestureDetector的dragView逻辑处理,那么就会尝试进行List的滑动,
每次滑动前,都会停止当前的滑动,这次的滑动使用了scroller这个类,scroller类本质上是一个计算器,替你计算出在一定时间一定速度的条件下应该的scroll值,
最开始还以为它内部也会跑一个thread来同步模拟或者类似Animator这种方式,后来发现其实根本就是根据流逝的时间来计算.
这里的stopScroll,也就是将scoller forceFinished<后面scroll前都会判断是否finish>,然后才是真正的scroll,
一样,在scroll的时候,也要判断是不是已经滑到顶/底<要考虑Vertical/Horizontal drag>,如果到了,就forceFinished. 否则scrollTo.
并且还会返回是否还能继续scroll.


<15>对fing的处理也是这样,被次fling没有被前面的ViewDrag逻辑处理,那么就用List的滑动逻辑处理,
直接调用scroller的fling函数开始模拟一次fling,然后调用定义的ListOnFling函数开始同步的调整UI,这个函数在最开始会检测scroller的computeScrollOffset来看
scroller模拟的操作是不是有更新值,如果没有<如果被forceFinished>,直接返回,否则就得到scroller的getCurrX/Y(),并scroll List,然后最重要的是检测mScroller.isFinished(),
<也会被forceFinished影响>如果还没有结束,就post一个同样的ListOnFling到下一个slice,实现了不断自动的scroll<这也是scroller的惯用使用法,因为scroller本身只管计算,
所以不可能scroller自己来不断的触发刷新UI,就只能由使用方通过这种finish检测+post下一个slice读取scroller值调整的办法来实现UI的自动刷新>.
这里使用的scroller是自己对Android scroller类的一个封装, 不过基本思路一样.


<16>移除某个TabView的动画也是利用valueAnimator实现的,这个实现逻辑也被封装在一个TouchController的函数中,
既然是通过Animator来实现位移,那么显然需要得到位移量,位移量根据drag的方向也不一样<移到屏幕的最左/右端>,利用MoveX/Y和当前drag的TabView的X/Y,就可以得到要位移到的坐标了.
位移的时间根据提供的velocity和moveX/Y得到,注意duration是ms,所以要乘以1000.同样的自定义了AnimatorUpdateListener,并在onAnimationUpdate中得到当前被drag的View应该移动到的坐标,
直接调用之前实现的dragTo函数,和之前一样,为了安全和简化,在移除动画开始的时候会将List disable, 而在动画结束以后恢复,这里因为UI设计的原因,还需要一个将空出来的位置逐渐
压缩的动画过程,还需要一个操作,也封装为了函数.


<17>压缩空位的过程主体也是一个ValueAnimator,但是会涉及到layout,首先为了保险,会再强制stopDrag,避免用户多余交互的影响.
如果已经没有TabView了,那么也就不需要这个过程了,直接调用删除最后一个Tab的特殊处理函数,然后将list恢复 Enable即可.
注意,这时候被remove的tabView其实还是在原来的位置没有动,只是INVISIBLE,为了实现这个空位慢慢被压缩的过程,就需要慢慢的调整这个被"删除"的tabView的宽/长<orentation决定>,
初始值直接设为TabView的measuredWidth/Height, 还是使用万金油valueAnimator来进行数值简便,而Interpolator按照设计的意见使用了AccelerateDecelerateInterpolator,
依旧是定制一个AnimatorUpdateListener, 在这个过程中,产品设计又提出了除了空位被压缩外,其他依然存在的TabView可以根据多出来的空间对自己的宽/长进行调整.
这一部分和现在的并不冲突,只是在调整被删除的TabView的size的同时,还要调整其他的TabView的size. 调用measure结合MeasureSpec.makeMeasureSpec(newWidth/Height, MeasureSpec.EXACTLY),
就可以为View指定下一次layout使用的尺寸<当然,有时候parent会强制改为自己任何合适的size,但是List本身的layout也由我实现,所以不必care>, 然后调用requestLayout触发measure->layout->draw<Android的一帧的工作包括处理输入/Animator/layout,因此在这里requestLayout,随后的Frame第三阶段就可以看到UI的更新>,
在Animator结束以后,出于性能的考虑,没有立即同步真正的removeTab<这回造成在新的Frame描画期间有太多的工作导致不流程>,而是post一个delay的Runnable来执行M层的remove以及恢复
List的Enable.


<18>被Drag的TabView复位也是同样的原理,并且意味没有压缩空位和尺寸调整,更加简单,不再赘述.  当时想过将这些相似过程抽离为一个通用的过程,
不过因为项目比较紧张,也没有冒险<TODO永远是传说>


<19>增加一个新的Tab,和刚才说的Remove UI过程基本相反,add TabView<一开始是INVISIBLE>,拓展出空位<不断调整尺寸layout>,最后将新View设为VISIBLE.
一个小插曲是设计又提出在新的TabView显现的过程中要加一个alpha过渡,直接通过后面衔接一个ObjectAnimator.ofFloat(view, "alpha", 0f, 1.f)实现了.
在全部动画做完以后,这个消息也会被触发出去,有兴趣的其他模块会响应.


<20>因为TabContainer作为一层比较多余的layout存在了,所以其实这里的 setEnabled还包括了对TabContainer的setEnabled<不喜欢,但是当时的权宜之计>.


<21>onMeasure函数实现:
一般来说,在parent onMeasure的时候,parent会调用每个childView的measure为其给予建议尺寸,childView的onMeasure会结合此和自己的实际需求设置自己的measuredWidth/Height,
不过在本模块开发中,因为在onMeasure外通过childView的measure就已经为其确定了尺寸,那么在onMeasure中,就不再对childView进行干涉,而是直接通过getMeasuredWidth/Height来
读取之前已经为其设好的size,并根据这些size决定自己的size<setMeasuredDimension>.
当然,List在onMeasure的时候,parent其也会传入widthMeasureSpec & heightMeasureSpec, 通过MeasureSpec.getSize()可以得到给予的建议值.
但是这个建议值不能直接采用,要考虑当前的ChildView的状况.
按照orentation将每个ChildView的measuredHeght/Width相加<还会额外插一个divider>,就得到要容纳这些childView所需要的width/height,
这时候和给予的建议值进行比较,如果算出的width/height比建议值小,采用计算值<不用那么大>, 如果大,则采用建议值<建议值基本是全屏了,只能这么多,再大也没意义了>.
setMeasuredDimension.


<22>onLayout函数的实现,onLayout函数才是真正的为childView指定位置和尺寸的地方.
因为是AdapterView,因此要先检测是否已经设置了Adapter<childView的来源>,没设置的话,直接super.onLayout<啥都没干>,还要加一个flag看是不是第一次layout<一般是第一显示出来的时候>,
如果是,那么替List选中Active的TabView.
如果有adapter,就可以根据Adapter来获取childView并为其layout了. 直接使用了childView的measuredHeight/Width<一般来说,这个值只是参考值>, 因为是List,所以layout也就是简单的
按照List从上到下/从左到右排下来.
在onLayout中,其实并没有使用到Adapter来获取View,只是对现有的childView做了处理,而真正使用Adapter获得View,并addViewInLayout没有在在这里做,应该在之前的某个阶段进行.
onLayout的职能应该单纯化,不负责Add/RemoveView,只做自己该做的layout工作.


<23>从AdapterView中取得childView并addInLayout是另外一个函数fillViews做的<这一步不会触发UI刷新>:
先将所有的childView Remove<removeAllViewsInLayout>,再调用Adapter的getCount得到现在的M有几个childView的映射,
然后遍历count通过Adapter的getView<convertView设为null,没有实现这一部分,不过在adapter里实现了类似的机制>获得对应id的View.
这个View还要根据当前的count等因素调整自己的size<通过Measure生效>. 最后生成一个LayoutParams<先尝试从View获取,如果没有再new>
(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT),并作为addViewInLayout的参数<注意要forbidLayout=true>,


<24>一次UI根据M刷新的流程: M更新->ListView的notifyDataSetChanged->Adapter的notifyDataSetChanged->DataSetObserver的onChanged->fillViews+requestLayout.


<25>List还提供了对TabView的内容进行update的转发调用,update时,TabView会根据当前的情况改变TabView的内容<其实是Adapter所作的>.


<26>List还提供了更换对应M的调用,会转发至Adapter,然后调用notifyDataSetChanged来更新UI显示新的TabView.


<27>List也提供了对orentation变化的响应,这个消息会被进一步转发给TouchController和Adapter, 最后requestLayout来刷新UI.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值