從啟動界面到主界面之後的效果如圖所示,採用的是v4包下的DrawerLayout, activity_main.xml文件如下: ...
從啟動界面到主界面之後的效果如圖所示,採用的是v4包下的DrawerLayout, activity_main.xml文件如下:
<!-- A DrawerLayout is intended to be used as the top-level content view using match_parent for both width and height to consume the full space available. -->
<android.support.v4.widget.DrawerLayoutandroid:id="@+id/drawer_layout"xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="net.oschina.app.ui.MainActivity"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><FrameLayoutandroid:id="@+id/realtabcontent"android:layout_width="match_parent"android:layout_height="0dip"android:layout_weight="1"/><FrameLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:background="?attr/windows_bg"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginBottom="4dip"><net.oschina.app.widget.MyFragmentTabHostandroid:id="@android:id/tabhost"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="4dip"/><Viewandroid:layout_width="match_parent"android:layout_height="1px"android:background="?attr/lineColor"/></RelativeLayout><!-- 快速操作按鈕 -->
<ImageViewandroid:id="@+id/quick_option_iv"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:contentDescription="@null"android:src="@drawable/btn_quickoption_selector"/></FrameLayout></LinearLayout><!-- 左側側滑菜單 -->
<fragmentandroid:id="@+id/navigation_drawer"android:name="net.oschina.app.ui.NavigationDrawerFragment"android:layout_width="@dimen/navigation_drawer_width"android:layout_height="match_parent"android:layout_gravity="start"tools:layout="@layout/fragment_navigation_drawer"/></android.support.v4.widget.DrawerLayout>
側邊欄佈局fragment_navigation_drawer.xml的定義:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:background="?attr/layout_bg_normal" ><net.oschina.app.widget.CustomerScrollViewandroid:layout_width="match_parent"android:layout_height="0dip"android:layout_weight="1"><include layout="@layout/fragment_navigation_drawer_items"/></net.oschina.app.widget.CustomerScrollView ><include layout="@layout/fragment_navigation_drawer_foot"/></LinearLayout>
fragment_navigation_drawer_items.xml和fragment_navigation_drawer_foot.xml的佈局比較簡單,不再贅述。
主要介紹自定義控制項CustomerScollView,繼承了ScrollView:
package net.oschina.app.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.ScrollView;
/**
* 可以拖動的ScrollView**/public class CustomerScrollView extends ScrollView {private static final int size = 4;private View inner;
private float y;private Rect normal = new Rect();public CustomerScrollView(Context context) {
super(context);
}public CustomerScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}@Overrideprotected void onFinishInflate() {if (getChildCount() > 0) {
inner = getChildAt(0);}}@SuppressLint("ClickableViewAccessibility")
@Overridepublic boolean onTouchEvent(MotionEvent ev) {if (inner == null) {return super.onTouchEvent(ev);} else {
commOnTouchEvent(ev);}return super.onTouchEvent(ev);}public void commOnTouchEvent(MotionEvent ev) {int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
y = ev.getY();break;
case MotionEvent.ACTION_UP:
if (isNeedAnimation()) {
// Log.v("mlguitar", "will up and animation");
animation();}break;
case MotionEvent.ACTION_MOVE:
final float preY = y;float nowY = ev.getY();
/**
* size=4 表示 拖動的距離為屏幕的高度的1/4*/int deltaY = (int) (preY - nowY) / size;// 滾動
// scrollBy(0, deltaY);
y = nowY;if (isNeedMove()) {
if (normal.isEmpty()) {
normal.set(inner.getLeft(), inner.getTop(),inner.getRight(), inner.getBottom());return;
}int yy = inner.getTop() - deltaY;
// 移動佈局
inner.layout(inner.getLeft(), yy, inner.getRight(),inner.getBottom() - deltaY);}break;
default:
break;
}}public void animation() {TranslateAnimation ta = new TranslateAnimation(0, 0, inner.getTop(),
normal.top);ta.setDuration(200);inner.startAnimation(ta);inner.layout(normal.left, normal.top, normal.right, normal.bottom);normal.setEmpty();}public boolean isNeedAnimation() {return !normal.isEmpty();
}public boolean isNeedMove() {int offset = inner.getMeasuredHeight() - getHeight();
int scrollY = getScrollY();
if (scrollY == 0 || scrollY == offset) {
return true;}return false;}}
再來看看效果:
可以看到向上滑動的時候,會有明顯回彈的效果。
oschina客戶端滑動菜單的View的佈局使用了可以拖拽的ScrollView,類文件為CustomerScrollView。
- 拖拽的目標是ScrollView內的菜單的佈局View,所以在CustomerScrollView內的onFinishInflate()函數中首先通過getChildAt(0)來獲取菜單佈局的View,這就是第一步的目標是獲取要拖拽的對象。onFinishInflate()載入完view,這裡的View指的就是源碼中<include layout="@layout/fragment_navigation_drawer_items"/>載入的佈局文件。
- 拖拽的過程實際上是一個“按下-移動-抬起”的過程,因此要重寫onTouchEvent(MotionEvent ev),其中移動過程實際上是將菜單view按照移動的方向和距離,怎麼實現這個功能呢?源碼中最關鍵的就是這行代碼inner.layout(inner.getLeft(), yy, inner.getRight(),inner.getBottom() - deltaY);這個方法四個參數都是inner相對其父控制項ScrollView的坐標原點而言的。不是很瞭解的,可以專門查查坐標的相關知識。
- 當手指抬起也就是MotionEvent.ACTION_UP事件發生時,將拖拽後的view恢復移動到原來位置,移動過程附加了一個動畫,由於移動實際上是位置發生了變化,因此用到了TranslateAnimation,因為是上下拖拽,所以X的起始和終止坐標都是0,Y的起始和終止坐標至於為什麼那麼寫,相信看完博客應該就會明白了。那麼問題來了,要自動移動回去,那麼觸發的時機在MotionEvent.ACTION_UP中,原來的位置怎麼保存,因為移動時需要左上右下四個參數,因此在CustomerScrollView中我們看到了這樣一個變數private Rect normal = new Rect();通過normal.set(inner.getLeft(), inner.getTop(),inner.getRight(), inner.getBottom());方法記錄菜單view的初始化位置。
- 經過仔細揣摩發現scrollY == 0這個條件實際上是滾動到了最頂部的時候,而scrollY == offset是滾動到最底部的時候,兩個條件滿足其中一個都可以實現拖拽的效果。int offset = inner.getMeasuredHeight() - getHeight();相當於本身的身高減去實際能看到的身高就等於沒有看到的身高部分。
//是否需要移動
public boolean isNeedMove() {int offset = inner.getMeasuredHeight() - getHeight();
int scrollY = getScrollY();
if (scrollY == 0 || scrollY == offset) {
return true;}return false;}
- 為什麼源碼中需要isNeedAnimation()這個函數呢?因為恢復到原來位置也用到了inner.layout(normal.left, normal.top, normal.right, normal.bottom);,因此normal首先必須要有四個參數值。而這個normal只有滿足上面的條件後才有值的。
- 為什麼在拖拽發生又恢復到原來位置後,要把這個normal.setEmpty();置空呢?它的意圖是什麼?仔細想來,發現這個normal的set左上右下四個值時,是在滿足2.4兩種條件之一就會有具體值的。因此這個normal就會有兩種不同的Rect.頂部的時候左上右下四個值分別為(0,0,實際菜單的寬度240dp,菜單的實際測量高度)而滾動到最底部的時候左上右下四個值分別為(0,負的【菜單的實際高度減去屏幕的高度】,實際菜單的寬度240dp,屏幕的高度),因此需要清空。