最近要开发一款TV上的Launcher,需求上要有三个类似三明治的页面,可以循环滚动,让用户自由切换。大致的样式是下面这样的。中间的是显示区域。
想过使用android原生的Launcher。但是分析了下,比较复杂,需要花费时间去理解和学习,由于任务紧迫,而且有特殊的定制要求所以决定采用ViewGroup去实现。下面就详细解决我是是如何实现的。
首先,我在代码中建立了一个“自定义控件”,这个控件继承与ViewGroup,额。。。如果不知道怎么什么是自定义控件,构建方式如下:
package com.xxx.xxx; public class ScrollLayout extends ViewGroup { public ScrollLayout(Context context, AttributeSet attrs, int defStyle) {} }
一旦构建完毕,我们就需要添加所需的三个页面到这个viewgroup中了,需要自己定义三个layout.xml文件作为自己的三个page,怎么创建这里不多说。
添加方式有2种,一种是死的,一种是活的。
先说死的。在viewgrop所在的layout.xml文件中手动添加
<com.xxx.xxx.view.ScrollLayout> <sublayout1 /> <sublayout2 /> <sublayout3 /> </com.xxx.xxx.view.ScrollLayout>
这样就添加完成了,好处是比较简单,坏处是灵活性太差。
还有一种是活的方式,这里推荐还是使用活的方式:
public ScrollLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mScroller = new Scroller(context, new AccelerateInterpolator(), true); // LayoutInflater inflater = // LayoutInflater.from(context); TvLayout tvLayout = new TvLayout(context, attrs); VodLayout vodLayout = new VodLayout(context, attrs); AppLayout appLayout = new AppLayout(context, attrs); this.addView(tvLayout); this.addView(vodLayout); this.addView(appLayout); }
如上的代码就是动态添加三个子页面。为什么采用“活”的方式,我在后面来说明。
当我们将三个页面添加完成后,它在viewgroup的顺序就定义下来了,以0开始排列。可以通过getchildAtIndex(int index)进行获取
添加完成后,我们接下来就需要将三个页面的整体位置按照最上面的图固定下来。这里就需要使用ViewGroup的onLayout的重载函数去实现。
首先看下原理图
上面是一开始的默认位置(图不是非常准确,只是用于说明),如果我们在onLayout中不加入偏移量EDGE_PADDING。下面是加入偏移量后的位置
我们电视的分辨率是1920*1080的分辨率,所以默认排列的位置是从x=0到x=1920*3的位置进行排列。
由于我的三个页面有个特殊效果,就是要显示相邻两页(TV, APP)部分页的内容。所以在排列上还有小的改动。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childLeft = 0; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View childView = getChildAt(i); if (childView.getVisibility() != View.GONE) { final int childWidth = childView.getMeasuredWidth(); childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight()); childLeft += childWidth + EDGE_PADDING;//这个就是EDGE_PADDING就是细微的改动,也就是偏移量. } } }
这里需要提一下,我的三个页面,不是1920*1080的大小,而是一个1080*1080大小的区域。
现在我们的三个页面都已经布局完成了,但是如何默认将第二个页面作为初始的显示页面呢?
一开始的初始显示页面,系统默认是将0-1920这个区域作为显示区域的。这里就需要移动显示区域。当然为了达到最上面的效果,也不是简单的移动1920像素的位置就可以了
移动的长度是小于1920的位置。
它的原理是在ViewGroup中的onMeasue里面实现的
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int width = WIDTH; int measueWidth = MeasureSpec.getMode(widthMeasureSpec) + width; final int count = getChildCount(); for (int i = 0; i < count; i++) { getChildAt(i).measure(measueWidth, heightMeasureSpec); } if (!isInit) { scrollTo(mCurScreen * width - INTERVAL, 0);//这句是关键,用于移动显示区域到第二个页面上面。interval是相邻的空隙页面。 isInit = true; } }
通过上面的实现我们就完成了初始的三屏显示页面的初始布局。用户一旦进入,就是最上面的那个图的效果。
下面就是移动了。
涉及到的移动无非就是左和右的移动,还有如何循环移动(滑到第一个,再滑可以显示第三个,滑到第三个后可以滑到第一个页面)
先说如何实现左右滑动。
whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));//whichscreen是传入参数,只有0和2两种选择,表示的是前滑还是后滑,如果有N个页面,则只有0到N-1这两个值 setMWidth(); int scrollX = getScrollX();//移动的位移,这个是宽度-interval得到的。来源于之前的scrollTo首次,后续会根据下面startScroll重新计算 int startWidth = whichScreen * mWidth;//计算开始宽度 getCurScreen().exit(); if (scrollX != startWidth) { int delta = 0; int startX = 0; if (whichScreen > mCurScreen) { 右移,因为移动屏幕比当前显示屏幕的index大 setPre(); delta = startWidth - scrollX - INTERVAL;//计算移动的长度,算上偏移,其实就是移动一个mWidth的距离 startX = mWidth - delta;//计算当前初始的位置. mWidth是真实的每个页面主布局的宽度,这里是1080 } else if (whichScreen < mCurScreen) { 左移 setNext();//控件重新排列,为滑动左准备,循环滑动关键 delta = -scrollX - INTERVAL;//移动一个-mWidth的距离 startX = scrollX + mWidth; } else { startX = scrollX; delta = startWidth - scrollX; } duration = 500; mScroller.startScroll(startX, 0, delta, 0, duration);//移动 invalidate(); // Redraw the layout
说完了移动,那么如何实现循环的滚动呢。。。我们看下代码
setNext向右滑动
private void setNext() { int count = this.getChildCount(); View view = getChildAt(count - 1); removeViewAt(count - 1); addView(view, 0); }
这里可以把整个布局看成链表结构,实际的行为就是当要滑动一个页面之前,如果向右滑动,先要将第三个位置的view从链表中移除,然后将其放入到整个链表的头部。
也就是说我们滑动的view永远是处于链表中index=1位置的view.这样就可以在滑动的时候循环滑动。
通过上面的步骤,就可以实现所有的效果了。
另外这里再提下,有的人想要获取一屏滚动完成的事件。找了很久我没找到。但是ViewGroup有个方法可以实现这个
protected void onScrollChanged(int l, int t, int oldl, int oldt) 这个重载函数。
大家可以判断I这个参数是否等于我们每次移动的最大距离来得到是否已经滚动完毕。