上一篇中我们已经一起学了怎么简单粗暴的撸个支持动态布局的网格控件出来,但在上一篇的介绍中,并没有学习实现网格控件的滑动效果,所以本篇就来讲讲,要如何让我们的网格控件可以支持自定义滑动策略。

效果

图一是Tv应用:当贝市场的主页

图二是咱自己撸的简单粗暴的 Tv 应用主界面网格控件:TvGridLayout 的示例,每个 Tab 下,每一屏的卡位大小、位置都是动态计算出来的。

实现

第一步:定义布局数据结构

第二步:自定义 TvGridLayout

第三步:自定义 Adapter

第四步:动态布局

第五步:初步使用

以上内容是在上一篇中讲解的内容,所以如果还没有看过上一篇的,建议先阅读上一篇一起撸个简单粗暴的Tv应用主界面的网格布局控件(上)。那么下面就开始我们今天的内容了:

第六步:内嵌 OverScroller 自定义滑动策略

首先,我们的网格控件是继承自 FrameLayout,那么它本身就是没有支持滑动的效果的,但是我们的网格控件又需要支持多屏显示,那么当焦点滑到当前屏之外时,自然就需要将下一屏的卡位滑动到屏幕内进行显示。

而实现滑动效果的方式有两种:

  • 将网格控件嵌套在 HorizontalScrollView
  • 自己在网格控件内部实现滑动效果

第一种方式实现最简单,我们只要将自己的网格控件 TvGridLayout 嵌套在 HorizontalScrollView 中,就可以实现滑动效果了。

虽然实现最简单,但缺点也很明显,就是滑动的策略只能按照 HorizontalScrollView 规则来,我们并没有办法进行修改。比如说,滑动的持续时长,滑动的距离,什么时候触发滑动等等。

产品的口味可是很刁钻的,单单使用默认的滑动策略,通常是很难满足产品的,虽然也可以通过一些反射等手段来修改 HorizontalScrollView 的默认实现,但有点复杂,且容易出问题。

本着不怕瞎折腾的精神,网格控件既然都已经自己撸了,那滑动的实现干脆也来自己撸好了。

6.1 实现滑动的方式

想要让一个控件滑动起来的方式很多很多:

  • 动画
  • ViewGroup#onLayout()
  • View#scrollTo(), View#scrollBy()
  • OverScroller

动画也行,重新对子 View 布局,修改子 View 位置也行,调用 View 自带的 scrollTo(), scrollBy() 也行,或者直接用系统提供的滑动辅助类 OverScroller 也行,都行,方式很多,只要能够让控件动起来就行。所以,让 View 动起来一直就不是个问题,问题是要怎么滑,什么时候滑,滑多长,滑多久,这些问题才是撸个滑动功能的问题所在。

6.2 HorizontalScrollView 滑动原理

既然滑动要自己撸,那当然是要先参考一下 Google 大神的实现思路了,所以首先就先来看看 HorizontalScrollView 的滑动原理是怎样的?

有一点需要先提一下的是,由于我们是着重分析 Tv 应用的滑动效果,也就是说是由遥控器来触发的滑动效果,那么 HorizontalScrollView 内部跟手指触摸相关的滑动原理就不分析了,着重分析跟 Tv 相关的滑动原理即可。

而 Tv 应用由于都是通过遥控器事件即 KeyEvent 来进行 ui 的交互,那么,理所当然,要查看 HorizontalScrollView 的滑动原理的话,就需要跟着 dispatchKeyEvent() 走下去应该就可以了。

//HorizontalScrollView#dispatchKeyEvent()
public boolean dispatchKeyEvent(KeyEvent event) {// 1. 如果事件没有被消耗掉,那么就交由滑动去处理return super.dispatchKeyEvent(event) || executeKeyEvent(event);
}public boolean executeKeyEvent(KeyEvent event) {...boolean handled = false;if (event.getAction() == KeyEvent.ACTION_DOWN) {switch (event.getKeyCode()) {//2. 事件是方向键左键时,那么就向左滑动case KeyEvent.KEYCODE_DPAD_LEFT:if (!event.isAltPressed()) {handled = arrowScroll(View.FOCUS_LEFT);}...}}return handled;
}public boolean arrowScroll(int direction) {...//3. 先寻找下个焦点的 viewView nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {//4. 根据下个焦点的位置,去计算是否需要进行滑动,需要的话那么计算滑动多长的距离nextFocused.getDrawingRect(mTempRect);offsetDescendantRectToMyCoords(nextFocused, mTempRect);int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);//5. 根据计算出来的滑动距离去处理滑动逻辑doScrollX(scrollDelta);nextFocused.requestFocus(direction);} ...return true;
}private void doScrollX(int delta) {if (delta != 0) {//6. HorizontalScrollView默认是开启了平衡滑动,但可也通过接口关掉if (mSmoothScrollingEnabled) {smoothScrollBy(delta, 0);} else {scrollBy(delta, 0);}}
}public final void smoothScrollBy(int dx, int dy) {...if (duration > ANIMATED_SCROLL_GAP) {//7. 如果处于边界情况,那么需要对计算出来的滑动长度进行修正,确保边界情况不会出问题final int width = getWidth() - mPaddingRight - mPaddingLeft;final int right = getChildAt(0).getWidth();final int maxX = Math.max(0, right - width);final int scrollX = mScrollX;dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;//8. 上述步骤均只是用于计算需要滑动的距离值,计算出来后滑动的实现交由mScroller处理mScroller.startScroll(scrollX, mScrollY, dx, 0);postInvalidateOnAnimation();}...
}//9. mScroller是OverScroller的实例
private OverScroller mScroller;

HorizontalScrollView 的滑动原理,例如是如何计算滑动距离的以及都有哪些会触发滑动的场景等等,就不深入去分析了,这不是本篇的目的,以后有时间再抽空来梳理。

所以,只需要跟着遥控器事件 dispatchKeyEvent() 走下去后,就可以找到原来 HorizontalScrollView 内部是通过 OverScroller 来实现的滑动效果。

而且,梳理了 HorizontalScrollView 从接收到遥控器事件到最终实现滑动的一个整体的流程后,我们再自己撸滑动的功能时,也可以参考这个思路、这个流程来写,所以这也是阅读源码的好处,大伙有时间得多抽抽时间来阅读源码多学习学习。

6.3 OverScroller 原理

既然 HorizontalScrollView 内部是通过 OverScroller 来处理滑动的相关逻辑的,那么,我们也用 OverScroller 来做好了,向 Google 大佬模仿借鉴。

网上关于 OverScroller 的使用教程很多,本篇就不着重讲了,要理解一点的是,OverScroller 只是一个滑动辅助类。

说得白一点也就是,我们只需要告诉 OverScroller 我们想滑动多长的距离,多久时间滑完,那么,OverScroller 内部就会根据每一帧的时间去计算当前帧时滑动的进度。然后,我们再每一帧通过 OverScroller 计算出的滑动进度,去作用到需要滑动的 View 上面来达到滑动的效果。

如果有看过我前面几篇关于动画的博客分析的话,那么上面这点就会很清楚了。OverScroller 实现滑动的整个流程原理跟属性动画的 ValueAnimator 非常相似,两个类内部都没有任何涉及 ui 的操作,两个类的作用都是用于根据当前帧的时间计算当前帧时的进度值。

唯一有区别的点就是,ValueAnimator 内部会自己通过 Choreographer 去监听每一帧的屏幕刷新信号,然后内部在接收到每一帧信号时就会自动去根据当前帧时间计算;而 OverScroller 内部并没有任何监听屏幕刷新信号的逻辑,也就是说,如果要使用 OverScroller 的话,我们需要在接收到每一帧的屏幕刷新信号时手动去通知 OverScroller,它才可以正确去工作

这就是为什么,大伙在网上搜 OverScroller 的使用教程时,基本每一篇都会提到说 OverScroller 需要跟 View 的 computeScroll() 一起使用的原因。

computeScroll() 是 View 中的一个空方法,在 draw() 方法中被调用。所以,只要我们能够让需要滑动的 View 在滑动的这段时间内,每一帧都通知 View 进行重绘刷新,那么它每一帧就都会走到 computeScroll(),这样我们就可以在 computeScroll() 中手动去通知 OverScroller,它内部就可以根据当前帧时间去计算滑动的工作了。

这也是为什么,大伙搜 OverScroller 的使用教程时,基本每篇也说了,在调用了 startScroll() 之后需要紧接着调用 View 的 postInvalidateOnAnimation() ,否则滑动就会失效的原因。因为我们只有通知了 View 需要重绘,computeScroll() 才会被调用,才可以再手动去通知 OverScroller 进行工作。

6.4 触发滑动的时机

搞清了 OverScroller 的原理后,那么如果要在我们自己的网格控件里撸滑动逻辑的话,也可以大概清楚需要撸哪些代码了。

因为 OverScroller 只负责根据我们指定的滑动距离和持续时长,在每一帧里去计算滑动进度的工作。那么,到底需要滑动多长的距离,持续多久,什么时候触发滑动,这三者就是自定义有滑动效果控件需要撸出来的代码了。

我们只针对 Tv 应用的话,显然,滑动的时机就在于遥控器事件了,这是第一点。

HorizontalScorllView 是在 dispatchKeyEvent()中,每次都去检查是否需要滑动,而满足滑动的条件则是下个焦点的 View 是否在屏幕上是可见的,而滑动的距离则是将这个不可见的 View 滑动到刚刚好全部可见。当然,它内部还有其它滑动策略,比如整页滑等等,但这些就需要手动去调用相关接口。

仅仅使用 HorizontalScrollView 默认的滑动效果很难满足产品需求,就像开头的当贝市场的示例图,很明显,它的滑动策略跟 HorizontalScrollView 就是不一样的,它是焦点快接近边缘时,就会去触发滑动了,即使下个焦点的 View 还是全部可见时。

6.5 自定义滑动策略

滑动的时机、滑动的策略、滑动的距离,这些并不是一成不变的,而是取决于业务场景需求;也是因为这样,才想到要自己撸个滑动的功能出来。

下面我会举个例子,将代码思路讲一下,但并不一定适用于你,所以大伙根据自己的需求自己撸一个就行了。

由于 Tv 应用都是通过遥控器控制,因此滑动的时机就在 dispatchKeyEvent()中进行检测就行了:

@Override
public boolean dispatchKeyEvent(KeyEvent event) {//1. 事件如果没有被消耗掉,那么就交由滑动去处理return super.dispatchKeyEvent(event) || executeKeyEvent(event);
}//这里的滑动策略:
//如果下个焦点的 View 属于另外一屏的话,那么就触发滑动
//滑动的距离为下一屏的宽度
//这里的下一屏是指上一篇提到的 ScreenEntity 数据模型,因为每个 Tab 下可能存在多屏数据,以屏作为单位来进行滑动,两焦点在两屏之间切换时,就触发滑动
private boolean executeKeyEvent(KeyEvent event) {int keyCode = event.getKeyCode();if (event.getAction() == KeyEvent.ACTION_DOWN) {...if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {//2. 检测下个焦点的 View 是否是属于另一屏中,是的话,将当前切换的这两屏的下标保存在 sTwoInt中if (checkIfOnBorder(FOCUS_LEFT, sTwoInt)) {...//3. 对外提供屏边界回调,当焦点在两屏之间切换时,触发回调if (mBorderListener != null && mBorderListener.onLeft(sTwoInt[0], sTwoInt[1])) {return true;}//4. 如果外部在接收到屏切换回调时,没有拦截,那么就去触发滑动scrollToPage(sTwoInt[1]);}} ...
}

上述是提供了一种滑动策略的思路,滑动策略并不一定需要按照系统默认的来,也不一定要按照上述来,适合自己的业务场景就行,不然干嘛要瞎折腾来自己撸这个滑动。

上述的滑动策略思路是当焦点在两屏之间切换时触发滑动,滑动的距离为下一屏的宽度。这种策略就完全不同于系统默认的策略,因此 HorizontalScrollView 就排不上用场了,那么就自己撸吧,不就是滑动的时机和滑动的距离计算要自己撸嘛,不难。

6.6 完工

以上,就将需要撸一个滑动的控件的思路讲完了。

小结一下,如果大伙想要自己撸个滑动的功能的话,很简单,可以用动画、scrollTo() 等方式;

如果大伙选择使用 OverScroller 的话,那么有几点需要注意:

  • OverScroller 只负责根据指定的滑动距离,持续时长来计算每一帧内的滑动进度
  • 因此我们需要在每一帧的屏幕刷新信号事件中手动去通知 OverScroller 进行工作,并取得经过它计算得到的当前帧的滑动进度来手动应用到 View 上
  • 这就是为什么使用 OverScroller 需要结合 View#computeScroll()一起使用,并且在调用了 startScroll() 之后需要紧接着调用 View#postInvalidateOnAnimation()的原因
  • 一个完整的滑动功能需要包括:触发滑动的时机、滑动策略、滑动距离的计算、OverScroller 辅助计算、应用到 View 上
  • 触发滑动的时机可以在 dispatchKeyEvent() 中进行检查是否满足滑动条件
  • 满足滑动的条件和滑动策略以及滑动距离的计算基于具体业务需求而实现
  • 整个流程设计可以参考 HorizontalScrollView 的源码


最近刚开通了公众号,想激励自己坚持写作下去,初期主要分享原创的Android或Android-Tv方面的小知识,感兴趣的可以点一波关注,谢谢支持~~

一起撸个简单粗暴的Tv应用主界面的网格布局控件(下)相关推荐

  1. android tv 开发布局,Android TV开发总结(七)构建一个TV app中的剧集列表控件

    前言:剧集类控件,在TV app中非常常见,今天将介绍构建一个TV app中的剧集列表控件,此控件上传到我的Github:https://github.com/hejunlin2013/Episode ...

  2. android+tv盒子+主界面,x96max+ 盒子 与 CoreELEC系统配置(三)AndroidTV刷机记录

    x96max+ 盒子 与 CoreELEC系统配置(三)AndroidTV刷机记录 2020-03-19 22:05:49 39点赞 211收藏 59评论 本文简要记录一下AndroidTV的刷机过程 ...

  3. android tv盒子 主界面,设计规范 | 详解Android TV用户界面设计

    文章对比电视界面,从主屏幕和应用程序两个方面对Android TV的用户界面设计进行了详细梳理,与大家分享. 与移动端设备不同,用户一般在3米外使用电视.因此电视界面设计上需要大而漂亮,要有合适的布局 ...

  4. android组件开关按钮,简单聊聊“开关”这个小控件

    开关虽然只是一个小控件,看起来很简单,但其实它的设计也有着大学问.本文和你一起探讨一下~ 一.开关是什么 开关,英文Switch,常被翻译为开关.滑动开关.切换开关,作为界面中可直接操作的元件,提供两 ...

  5. Android动态加载XML文件及控件来简单实现QQ好友印象的功能

    在android开发中,我们常常会遇到界面布局控件不确定的情况.由于某些功能的原因或者为了体现某些app的特色等这些原因会导致我们在实现界面布局时需要动态去加载一些控件,那么下面就来介绍一下如何用动态 ...

  6. Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件

    前言: 因为公司人员变动原因,导致了博主四个月没有动安卓,一直在做IOS开发,如今接近年前,终于可以花一定的时间放在安卓上了.好了,废话不多说,今天我们要带来的效果是苹果版本的QQ下拉刷新.首先看一下 ...

  7. 【Qt】控件——QPlainTextEdit使用简单介绍:常用方法及信号、逐行读取编辑框的内容、使用自带的快捷菜单、作为日志显示窗口

    Qt控件-QPlainTextEdit使用 参考链接: https://blog.csdn.net/seniorwizard/article/details/109726147; https://bl ...

  8. android组合控件的焦点,撸一个简单的TV版焦点控制的日历控件

    1.效果 最近需求要一个遥控控制的日历控件,找了半天没找到轮子,就自己撸一个,先看效果图: 效果图.gif 2.XML属性,所有属性默认为效果图 calender_textSize:星期和日期的字体大 ...

  9. 薅“刷宝”羊毛的autojs脚本教程,简单粗暴好用!!!

    介绍 本教程旨在帮助大家实现用手机或闲置手机来进行"刷宝"平台的收益,能薅多少米就看你的本事了,作者教你的是如何用autojs来进行脚本的挂机教程,掌握了这个思路后,你可以根据自己 ...

最新文章

  1. Python IDLE 无法启动
  2. 检测xcode工程中配置信息是否正确
  3. python传输大文件_python之socket运用之传输大文件
  4. uva 10723——Cyborg Genes
  5. OpenGL ES 2.0 for iPhone Tutorial
  6. linux c++ queue 多线程,C++多线程,消息队列用法
  7. 面试官不讲武德,居然让我讲讲蠕虫和金丝雀!
  8. css动画与js动画的区别
  9. windows登录linux免密码,Windows使用SSH Secure Shell实现免密码登录Linux的方法以及使用scp2命令免密码下载文件...
  10. 用Google XML Sitemaps为你的网站创建Sitemap
  11. latex algorithm 引用格式错误
  12. LVGL『Roller滚轮控件』介绍
  13. HTML+CSS小米注册登录界面
  14. SQL中的日期差函数
  15. avmovie_AVMovie1和AVMovie2
  16. 攻防世界 mfw 解题思路
  17. LaMDA 不可能觉醒吗?
  18. 将Maven仓库地址修改为阿里云的仓库地址
  19. 【CSS】尺寸和边框、盒子模型、外边距_02
  20. java中Date计算时间差

热门文章

  1. 自动化篇 - 躺着收钱!闲鱼自动发货机器人来啦~
  2. POJ 2411: Mondriaan's Dream
  3. 如何关闭计算机的f12功能键,win10如何关闭快捷键?win10关闭F1~F12快捷键的方法
  4. layer弹出层内点击确认提交数据并关闭弹出层
  5. QT信号槽第五个参数
  6. 干货 | 免费GIS数据网站推荐
  7. nvm安装node,但npm和node不识别
  8. 阿里巴巴2008校园招聘在线宣讲会
  9. 美团java工程师,成都外包面试笔试题
  10. 校园导游系统_C语言实现_Dijkstra(迪杰斯特拉算法)_数据结构