明天将Workspace和DragLayer联合起来进行阐述。
废话就不多说,Workspace的大体功能前面已有简介,现在具体说其中的技术点。
技术点 如何通过xml文件来构造自己定义的View组件?
在通过xml文件构造view组件的时候,往往都要使用到AttributeSet和defStyle这个两个参数。defStyleAttr是一个reference, 它指向当前Theme中的一个style, style其实就是各种属性的集合。
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Workspace, defStyle, 0);
// style就是各种属性的数组集合
mDefaultScreen = a.getInt(R.styleable.Workspace_defaultScreen, 2);
// 通过styleable name + _attr name得到属性值,如果没有定义则默认为2
a.recycle();
技术点 如何保证自定义的ViewGroup如Workspace按照我们想的那样横向显示3个(5个)屏幕?
要知道这一点我们必须先知道一个View是如何显示出来的,在显示View之前,我们应该要为其指定尺寸和分布,这些是在onMeasure和onLayout中调用完成,废话不多说,上代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 仅当ViewGroup为fill_parent才处于EXACTLY模式
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Workspace can only be EXACTLY mode."); }
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Workspace can only be EXACTLY mode."); }
for (int i = 0; i < count; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec); // 为所有子View设定标准尺寸
}
if (mFirstLayout) {
scrollTo(mCurrentScreen * width, 0);
mFirstLayout = false;
} //滚动到第三屏设为默认主屏。
}
在尺寸设定完毕后,我们需要对ViewGroup中所有子View的位置进行设定,以满足他们的布局。
代码
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int childLeft = 0;
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
final int childWidth = child.getMeasuredWidth();
child.layout(childLeft, 0, childLeft + childWidth,
child.getMeasuredHeight()); //很明显是吧? 0 - 320 | 320 - 640 | 640 - 960 ...(假设屏幕宽320)
childLeft += childWidth;
}
}
}
布局设定OK。这样我们就了解到是如何为Workspace设定了屏幕数,并为他们做好布局、定好尺寸。(实际上在每次滑动结束后都会调用他们)下面我们需要知道的是具体的切屏操作是如何完成,在切屏时,又做了什么?
技术点 各屏幕被遮挡后如何重新获取焦点?
我们知道,View一般通过requestfocus获取焦点,当然前提是他们“能够”获得。通过复写addFocusables函数来为当前ViewGroup中的所有子View添加焦点获取能力 - -。然后当屏幕被遮盖又恢复后,ViewGroup会首先收到requestfocus消息,并在下列代码中作出相应处理:
代码
@Override
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) { int focusableScreen;
if (mNextScreen != INVALID_SCREEN) {
focusableScreen = mNextScreen;
} else {
focusableScreen = mCurrentScreen;
}
getChildAt(focusableScreen).requestFocus(direction, previouslyFocusedRect);
return false;
}
应该是很清晰了,函数名称起的很好,即当前ViewGroup树形结构中的某个View获取焦点。条件判断主要是用于检测是否发生过切屏。
技术点 在发生切屏时,是如何进行绘制和切换屏幕的工作?
有细心的朋友肯定已经注意到,在onMeasue函数调用中有一个scrollTo函数,用于将屏幕移动到指定坐标位置。程序初次启动时,显示默认第三屏便是在这里做了滚动操作,它会首先获得焦点。由ViewGroup调用dispatchDraw()操作,进行绘制和draw动作的传递。这里补充一个知识,除非ViewGroup本身有背景,否则调用onDraw是无效的。
在dispatchDraw操作中,判断是否处于滑动状态,通过drawChild()来完成对当前主屏和滑动状态的下一主屏进行绘制。否则,当切屏完成后,屏幕上不会有Widget或Shortcut,但是点击他们的位置却能启动相应程序。
滑动屏幕时有两个个非常重要的函数即scrollTo、scrollBy,我看了很多地方都没有对他们解释的很清楚。scrollTo, scrollBy其实都是在对View的内容进行移动。在Touch Move中发生scrollBy后并不会去调用computeScroll(),反而在move结束,up事件中调用startSrcoll,开始不断调整mScrollX, mSrcollY,将当前的基准点重新置回标准的:0,0 320,0 640,0...
简单的说下整体的绘制操作:接受onTouchEvent事件,当在移动中计算移动的距离,调整基准点并不停的绘制屏幕和移动壁纸,当touch结束后,通过startScroll将滑动完成,并将基准点置为标准点(在computeScroll中),更新重绘屏幕。
这样,Workspace的主要工作已基本算是完成,还有一些它担任委托的工作就没有介绍了,应该不会很复杂,看看就好。