众所周知,官方已经不推荐用TabActivity了,推荐用fragment,具体原因不去纠结。前段时间下载了开源中国的源码看了下,发现导航栏做法挺有意思的(或者很多人都这样做了只是我没发现),于是抽离了出来研究了下,下面来剖析具体做法。
先看布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#ffffff">
<include layout="@layout/main_header" />
<com.example.testandroid.ScrollLayout
android:id="@+id/main_scrolllayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1">
<include layout="@layout/frame_news" />
<include layout="@layout/frame_question" />
<include layout="@layout/frame_tweet" />
<include layout="@layout/frame_active" />
</com.example.testandroid.ScrollLayout>
<include layout="@layout/main_footer" />
</LinearLayout>
很简单,结构也很清晰就是一个LinearLayout包含三个布局,上面一个头layout,中间一个自定义的layout,下面一个footlayout。头layout不说了,里面内容可以任意写,甚至可以把这个layout去掉,footlayout代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:id="@+id/main_linearlayout_footer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:background="@drawable/widget_bar_bg_n">
<RadioButton
android:id="@+id/main_footbar_news"
style="@style/main_footbar_radio"
android:drawableTop="@drawable/widget_bar_news"/>
<ImageView
style="@style/main_footbar_cutline"
android:src="@drawable/widget_bar_cut_off"/>
<RadioButton
android:id="@+id/main_footbar_question"
style="@style/main_footbar_radio"
android:drawableTop="@drawable/widget_bar_question"/>
<ImageView
style="@style/main_footbar_cutline"
android:src="@drawable/widget_bar_cut_off"/>
<RadioButton
android:id="@+id/main_footbar_tweet"
style="@style/main_footbar_radio"
android:drawableTop="@drawable/widget_bar_tweet"/>
<ImageView
style="@style/main_footbar_cutline"
android:src="@drawable/widget_bar_cut_off"/>
<RadioButton
android:id="@+id/main_footbar_active"
style="@style/main_footbar_radio"
android:drawableTop="@drawable/widget_bar_active"/>
<ImageView
style="@style/main_footbar_cutline"
android:src="@drawable/widget_bar_cut_off"/>
<ImageView
android:id="@+id/main_footbar_setting"
style="@style/main_footbar_image"
android:src="@drawable/widget_bar_more"/>
</LinearLayout>
很简单,就是radiobutton,底部导航栏的布局。
重点在自定义布局ScrollLayout 。
先看onMeasure中是怎样计算每个view大小的:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//Log.e(TAG, "onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException(
"ScrollLayout only canmCurScreen run at EXACTLY mode!");
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException(
"ScrollLayout only can run at EXACTLY mode!");
}
// The children are given the same width and height as the scrollLayout
final int count = getChildCount();
for (int i = 0; i < count; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
// Log.e(TAG, "moving to screen "+mCurScreen);
scrollTo(mCurScreen * width, 0);
}
通过MeasureSpec.getSize和MeasureSpec.getMode分别得到了view的宽度和宽度模式。
onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值。我们需要通过int mode = MeasureSpec.getMode(widthMeasureSpec)得到模式,用int size= MeasureSpec.getSize(widthMeasureSpec)。
这个mode有三种:
UNSPECIFIED:父VIEW对子VIEW无任何约束,子VIEW可以为任意大小
EXACTLY :固定大小
AT_MOST:子VIEW可以达到最大size。
这里如果宽高的模式不是固定大小则抛出异常,scrollTo函数在这里被调用是可以打开应用默认为第N个view。
接着来看onLayout是怎样布局的:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();//得到子view的数目
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();//得到子view宽度,在这里得到的宽度是屏幕的宽度
Log.e(TAG, "childWidth:"+ childWidth+",childHeight:"+childView.getMeasuredHeight());
childView.layout(childLeft, 0, childLeft + childWidth,
childView.getMeasuredHeight());//布局子view位置,
childLeft += childWidth;
}
}
}
UI完成后再来看滑动逻辑(推荐先看我之前的关于手势分发的代码)。
onInterceptTouchEvent代码:
public boolean onInterceptTouchEvent(MotionEvent ev) {
//Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop);
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE)
&& (mTouchState != TOUCH_STATE_REST)) {//如果是move事件且view在运动状态,则直接返回true,手势由onTouchEvent处理
return true;
}
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_MOVE:
final int xDiff = (int) Math.abs(mLastMotionX - x);//滑动距离
if (xDiff > mTouchSlop) {
mTouchState = TOUCH_STATE_SCROLLING;//如果大于一个阈值,则把view状态设置为运动状态,然后把手势交给onTouchEvent处理
}
break;
case MotionEvent.ACTION_DOWN:
mLastMotionX = x;
mLastMotionY = y;
mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
: TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mTouchState = TOUCH_STATE_REST;
break;
}
return mTouchState != TOUCH_STATE_REST;
}
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
意思就是这个距离是我们认为用户想滑动屏幕之前的像素距离,通俗点来说就是超过这个值就可以判断用户是想滑动屏幕,而不是点击或者误触。
onTouchEvent代码就不贴了,里面有涉及到滑动速度问题,以后再分享。具体就是调用scrollBy移动view,使view能跟随手指滑动。
scrollBy和scrollTo的区别:
滑动到某一位置,为相对位置,如果参数为(10,10),则x和y方向分别滑动10单位距离,而不是滑动到(10,10)这个坐标。
滑动到某一位置,为绝对位置,如果参数为(10,10),则滑动到(10,10)这个坐标。
•scrollTo就是把View移动到屏幕的X和Y位置,也就是绝对位置。而scrollBy其实就是调用的scrollTo,但是参数是当前mScrollX和mScrollY加上X和Y的位置,所以ScrollBy调用的是相对于mScrollX和mScrollY的位置。我们在上面的代码中可以看到当我们手指不放移动屏幕时,就会调用scrollBy来移动一段相对的距离。而当我们手指松开后,会调用mScroller.startScroll(mUnboundedScrollX, 0, delta, 0,duration);来产生一段动画来移动到相应的页面,在这个过程中系统回不断调用computeScroll(),我们再使用scrollTo来把View移动到当前Scroller所在的绝对位置。
这样,子view就能根据跟随我们的手指滑动了,如果点击导航上的某个按钮,可以调用startScroll函数进行滑动动画,并且在滑动结束后调用OnViewChange接口来告诉Activity:我执行了滑动,你可以执行更新内容等动作了。
到此,导航栏布局已经完成,支持跟随手指滑动、滑动动画等,这种做法很简单也很实用。
网友评论