Android實現(xiàn)3D推拉門式滑動菜單源碼解析
前言
又看了郭霖大神的一篇博客《Android 3D滑動菜單完全解析,實現(xiàn)推拉門式的立體特效》,是關(guān)于自定義控件方面的,因為自己關(guān)于自定義控件了解的不過,以前的要求是會用就行,但是后來越發(fā)的明白只會用是不夠的,出現(xiàn)問題都不知道該怎么分析,所以我才打算把別人博客里的自定義控件的源碼給看懂,雖然可能時間花的時間長,但是,絕對是值得的!
因為源碼的東西比較多,看完之后發(fā)現(xiàn)還存在可以優(yōu)化的地方,郭神的代碼當時是為了例子講解,所以對這個控件類的封裝就沒有仔細去做,所以我就進行了封裝和優(yōu)化,是的移植到項目的時候會更加方便,解耦性更強。
實現(xiàn)
我們先來看一下示意圖:

下面我就來分析一下源碼。
從效果圖中可以看到的是,滑動的時候菜單會有一個效果,這個效果是沿y軸旋轉(zhuǎn)的效果,這種效果是用Matrix和Camera來實現(xiàn),具體怎么實現(xiàn)的我在另一篇文章《對Matrix中preTranslate()和postTranslate()的理解》中做了簡單的說明,可以很容易的實現(xiàn)這樣的效果。
在Image3DView中,我們封裝了這樣的效果,只要傳入左側(cè)菜單界面的View,然后就可以實現(xiàn)了。
先來看一下布局文件:
<com.example.sliding3dlayout.Sliding3DLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/slidingLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_height="fill_parent"
android:layout_width="240dp"
android:background="#333333"
android:visibility="invisible"
>
<LinearLayout
android:layout_centerInParent="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="50dp"
android:text="登錄"
android:gravity="center"
android:textColor="#ffffff"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="50dp"
android:text="注冊"
android:gravity="center"
android:textColor="#ffffff"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="50dp"
android:text="退出"
android:gravity="center"
android:textColor="#ffffff"
/>
</LinearLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/content"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_alignParentRight="true"
android:background="#ffffff"
android:orientation="vertical">
<Button
android:id="@+id/menuButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Menu" />
<ListView
android:id="@+id/contentList"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:cacheColorHint="#00000000" >
</ListView>
</LinearLayout>
</com.example.sliding3dlayout.Sliding3DLayout>
Sliding3DLayout類是定義的該菜單控件,里面有兩個主要的視圖,第一個是菜單視圖,第二個就是主界面視圖。當滑動的時候,我們把左側(cè)的菜單視圖隱藏,然后顯示Image3DView控件,也就是沿y軸旋轉(zhuǎn),根據(jù)滑動的距離,旋轉(zhuǎn)的角度在不斷變化,Image3DView的視圖也在不斷的變化,當菜單完全顯示的時候,就顯示左側(cè)菜單的界面,然后將Image3DView隱藏,這樣就實現(xiàn)了所謂的滑動動畫。
public class Sliding3DLayout extends RelativeLayout implements OnTouchListener{
//滾動顯示和隱藏左側(cè)布局時,手指滑動需要達到的速度。
public static final int SNAP_VELOCITY = 200;
//滑動狀態(tài)的一種,表示未進行任何滑動。
public static final int DO_NOTHING = 0;
//滑動狀態(tài)的一種,表示正在滑出左側(cè)菜單。
public static final int SHOW_MENU = 1;
//滑動狀態(tài)的一種,表示正在隱藏左側(cè)菜單。
public static final int HIDE_MENU = 2;
//記錄當前的滑動狀態(tài)
private int slideState;
//屏幕寬度值。
private int screenWidth;
//右側(cè)布局最多可以滑動到的左邊緣。
private int leftEdge = 0;
//右側(cè)布局最多可以滑動到的右邊緣。
private int rightEdge = 0;
//在被判定為滾動之前用戶手指可以移動的最大值。
private int touchSlop;
//記錄手指按下時的橫坐標。
private float xDown;
//記錄手指按下時的縱坐標。
private float yDown;
//記錄手指移動時的橫坐標。
private float xMove;
//記錄手指移動時的縱坐標。
private float yMove;
//記錄手機抬起時的橫坐標。
private float xUp;
//左側(cè)布局當前是顯示還是隱藏。只有完全顯示或隱藏時才會更改此值,滑動過程中此值無效。
private boolean isLeftLayoutVisible;
//是否正在滑動。
private boolean isSliding;
//是否已加載過一次layout,這里onLayout中的初始化只需加載一次
private boolean loadOnce;
//左側(cè)布局對象。
private View leftLayout;
//右側(cè)布局對象。
private View rightLayout;
//在滑動過程中展示的3D視圖
private Image3DView image3dView;
//用于監(jiān)聽側(cè)滑事件的View。
private View mBindView;
//左側(cè)布局的參數(shù),通過此參數(shù)來重新確定左側(cè)布局的寬度,以及更改leftMargin的值。
private MarginLayoutParams leftLayoutParams;
//右側(cè)布局的參數(shù),通過此參數(shù)來重新確定右側(cè)布局的寬度。
private MarginLayoutParams rightLayoutParams;
//3D視圖的參數(shù),通過此參數(shù)來重新確定3D視圖的寬度。
private ViewGroup.LayoutParams image3dViewParams;
//用于計算手指滑動的速度。
private VelocityTracker mVelocityTracker;
public Sliding3DLayout(Context context, AttributeSet attrs){
super(context, attrs);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
screenWidth = wm.getDefaultDisplay().getWidth();
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
public Sliding3DLayout(Context context){
this(context,null);
}
/**
* 左側(cè)布局是否完全顯示出來,或完全隱藏,滑動過程中此值無效。
* @return 左側(cè)布局完全顯示返回true,完全隱藏返回false。
*/
public boolean isLeftLayoutVisible(){
return isLeftLayoutVisible;
}
/**
* 綁定監(jiān)聽側(cè)滑事件的View,即在綁定的View進行滑動才可以顯示和隱藏左側(cè)布局。
* @param v
* 需要綁定的View對象。
*/
public void setScrollEvent(View v){
mBindView = v;
mBindView.setOnTouchListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event){
createVelocityTracker(event);
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
xDown = event.getRawX();
yDown = event.getRawY();
slideState = DO_NOTHING ;
break;
case MotionEvent.ACTION_MOVE:
// 手指移動時,對比按下時的橫坐標,計算出移動的距離,來調(diào)整右側(cè)布局的leftMargin值,從而顯示和隱藏左側(cè)布局
xMove = event.getRawX();
yMove = event.getRawY();
int moveDistanceX = (int)(xMove - xDown);
int moveDistanceY = (int)(yMove - yDown);
checkSlideState(moveDistanceX, moveDistanceY);
switch(slideState){
case SHOW_MENU:
rightLayoutParams.rightMargin = -moveDistanceX;
onSlide();
break;
case HIDE_MENU:
rightLayoutParams.rightMargin = rightEdge - moveDistanceX;
onSlide();
break;
default:
break;
}
break;
case MotionEvent.ACTION_UP:
xUp = event.getRawX();
int upDistanceX = (int)(xUp - xDown);
if(isSliding){
switch (slideState){
case SHOW_MENU:
if(shouldScrollToLeftLayout()){
scrollToLeftLayout();
}else{
scrollToRightLayout();
}
break;
case HIDE_MENU:
if(shouldScrollToRightLayout()){
scrollToRightLayout();
}else{
scrollToLeftLayout();
}
break;
}
}else if (upDistanceX < touchSlop && isLeftLayoutVisible){
scrollToRightLayout();
}
recycleVelocityTracker();
break;
}
if (v.isEnabled()){
if (isSliding){
unFocusBindView();
return true;
}
if (isLeftLayoutVisible) {
return true;
}
return false;
}
return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(changed&&!loadOnce){
//獲取左側(cè)菜單布局
leftLayout = getChildAt(0);
leftLayoutParams = (MarginLayoutParams)leftLayout.getLayoutParams();
rightEdge = -leftLayoutParams.width;
//獲取右側(cè)布局
rightLayout = getChildAt(1);
rightLayoutParams = (MarginLayoutParams)rightLayout.getLayoutParams();
rightLayoutParams.width = screenWidth;
rightLayout.setLayoutParams(rightLayoutParams);
image3dView = new Image3DView(getContext());
/*ViewGroup.LayoutParams params = new LayoutParams(android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT);*/
image3dView.setVisibility(INVISIBLE);
addView(image3dView);
// 將左側(cè)布局傳入3D視圖中作為生成源
image3dView.setSourceView(leftLayout);
loadOnce = true;
}
}
/**
* 回收VelocityTracker對象。
*/
private void recycleVelocityTracker() {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
/**
* 將屏幕滾動到左側(cè)布局界面,滾動速度設(shè)定為10.
*/
public void scrollToLeftLayout(){
image3dView.clearSourceBitmap();
new ScrollTask().execute(-10);
}
/**
* 將屏幕滾動到右側(cè)布局界面,滾動速度設(shè)定為-10.
*/
public void scrollToRightLayout(){
image3dView.clearSourceBitmap();
new ScrollTask().execute(10);
}
/**
* 獲取手指在右側(cè)布局的監(jiān)聽View上的滑動速度。
*
* @return 滑動速度,以每秒鐘移動了多少像素值為單位。
*/
private int getScrollVelocity() {
mVelocityTracker.computeCurrentVelocity(1000);
int velocity = (int) mVelocityTracker.getXVelocity();
return Math.abs(velocity);
}
/**
* 判斷是否應(yīng)該滾動將左側(cè)布局展示出來。如果手指移動距離大于屏幕的1/2,或者手指移動速度大于SNAP_VELOCITY,
* 就認為應(yīng)該滾動將左側(cè)布局展示出來。
*
* @return 如果應(yīng)該滾動將左側(cè)布局展示出來返回true,否則返回false。
*/
private boolean shouldScrollToLeftLayout() {
return xUp - xDown > leftLayoutParams.width / 2 || getScrollVelocity() > SNAP_VELOCITY;
}
/**
* 判斷是否應(yīng)該滾動將右側(cè)布局展示出來。如果手指移動距離加上leftLayoutPadding大于屏幕的1/2,
* 或者手指移動速度大于SNAP_VELOCITY, 就認為應(yīng)該滾動將右側(cè)布局展示出來。
*
* @return 如果應(yīng)該滾動將右側(cè)布局展示出來返回true,否則返回false。
*/
private boolean shouldScrollToRightLayout(){
return xDown - xUp > leftLayoutParams.width / 2 || getScrollVelocity() > SNAP_VELOCITY;
}
/**
* 執(zhí)行滑動過程中的邏輯操作,如邊界檢查,改變偏移值,可見性檢查等。
*/
private void onSlide(){
checkSlideBorder();
rightLayout.setLayoutParams(rightLayoutParams);
image3dView.clearSourceBitmap();
image3dViewParams = image3dView.getLayoutParams();
image3dViewParams.width = -rightLayoutParams.rightMargin;
//滑動的同時改變3D視圖的大小
image3dView.setLayoutParams(image3dViewParams);
showImage3dView();
}
public void toggle(){
if(isLeftLayoutVisible())
scrollToRightLayout();
else
scrollToLeftLayout();
}
/**
* 保證此時讓左側(cè)布局不可見,3D視圖可見,從而讓滑動過程中產(chǎn)生3D的效果。
*/
private void showImage3dView() {
if (image3dView.getVisibility() != View.VISIBLE) {
image3dView.setVisibility(View.VISIBLE);
}
if (leftLayout.getVisibility() != View.INVISIBLE) {
leftLayout.setVisibility(View.INVISIBLE);
}
}
/**
* 在滑動過程中檢查左側(cè)菜單的邊界值,防止綁定布局滑出屏幕。
*/
private void checkSlideBorder(){
if (rightLayoutParams.rightMargin > leftEdge){
rightLayoutParams.rightMargin = leftEdge;
} else if (rightLayoutParams.rightMargin < rightEdge) {
rightLayoutParams.rightMargin = rightEdge;
}
}
/**
* 根據(jù)手指移動的距離,判斷當前用戶的滑動意圖,然后給slideState賦值成相應(yīng)的滑動狀態(tài)值。
*
* @param moveDistanceX
* 橫向移動的距離
* @param moveDistanceY
* 縱向移動的距離
*/
private void checkSlideState(int moveDistanceX, int moveDistanceY) {
if (isLeftLayoutVisible) {
//如果是向左滑動,則是想隱藏菜單
if (!isSliding && Math.abs(moveDistanceX) >= touchSlop && moveDistanceX < 0) {
isSliding = true;
slideState = HIDE_MENU;
}
}//向右滑動則是顯示菜單
else if (!isSliding && Math.abs(moveDistanceX) >= touchSlop && moveDistanceX > 0
&& Math.abs(moveDistanceY) < touchSlop) {
isSliding = true;
slideState = SHOW_MENU;
}
}
/**
* 創(chuàng)建VelocityTracker對象,并將觸摸事件加入到VelocityTracker當中。
*
* @param event
* 右側(cè)布局監(jiān)聽控件的滑動事件
*/
private void createVelocityTracker(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
class ScrollTask extends AsyncTask<Integer, Integer, Integer>{
@Override
protected Integer doInBackground(Integer... speed){
int rightMargin = rightLayoutParams.rightMargin;
// 根據(jù)傳入的速度來滾動界面,當滾動到達左邊界或右邊界時,跳出循環(huán)。
while(true){
rightMargin+=speed[0];
if (rightMargin < rightEdge) {
rightMargin = rightEdge;
break;
}
if (rightMargin > leftEdge) {
rightMargin = leftEdge;
break;
}
publishProgress(rightMargin);
// 為了要有滾動效果產(chǎn)生,每次循環(huán)使線程睡眠5毫秒,這樣肉眼才能夠看到滾動動畫。
sleep(5);
}
if (speed[0] > 0){
isLeftLayoutVisible = false;
} else {
isLeftLayoutVisible = true;
}
isSliding = false;
return rightMargin;
}
@Override
protected void onProgressUpdate(Integer... rightMargin) {
rightLayoutParams.rightMargin = rightMargin[0];
rightLayout.setLayoutParams(rightLayoutParams);
image3dViewParams = image3dView.getLayoutParams();
image3dViewParams.width = -rightLayoutParams.rightMargin;
image3dView.setLayoutParams(image3dViewParams);
showImage3dView();
unFocusBindView();
}
@Override
protected void onPostExecute(Integer rightMargin){
rightLayoutParams.rightMargin = rightMargin;
rightLayout.setLayoutParams(rightLayoutParams);
image3dView.setVisibility(INVISIBLE);
if (isLeftLayoutVisible){
leftLayout.setVisibility(View.VISIBLE);
}
}
}
/**
* 使用可以獲得焦點的控件在滑動的時候失去焦點。
*/
private void unFocusBindView() {
if (mBindView != null) {
mBindView.setPressed(false);
mBindView.setFocusable(false);
mBindView.setFocusableInTouchMode(false);
}
}
/**
* 使當前線程睡眠指定的毫秒數(shù)。
*
* @param millis
* 指定當前線程睡眠多久,以毫秒為單位
*/
private void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在Sliding3DLayout中傳入了一個View,這個View是效果圖中的ListView,為什么要傳入這個View呢?因為我們要監(jiān)測滑動,也就是在ListView的滑動,然后根據(jù)這個滑動來判斷是否要顯示菜單,但是這樣實際出現(xiàn)了問題,我們稍后再說這個問題。
在Sliding3DLayout中總共有3個View對象,一個是左側(cè)的菜單View,一個是主界面的View,最后一個就是Image3DView,在onLayout方法里面我們要得到這三個對象,前兩個我們可以在xml布局文件里面得到,因為在Sliding3DLayout里面我們寫了,而Image3DView沒有寫,所以要生成一個對象,然后調(diào)用addView方法加入到Sliding3DLayout里面。接下來我們需要得到的就是MarginLayoutParams對象,包括主界面View的和Image3DView對象的MarginLayoutParams。為什么需要MarginLayoutParams對象,因為得到一個View的MarginLayoutParams對象,就可以設(shè)置rightMargin屬性的值,這個值是View距離右邊的距離,如果把該值設(shè)置成負數(shù)的話,拿主界面來說,rightLayout.setLayoutParams(rightLayoutParams);調(diào)用這個方法,主界面就會向右偏移一定的距離,從而實現(xiàn)主界面隨手指向右滑動而滑動,從而實現(xiàn)動畫的連續(xù)性。
在實現(xiàn)的時候,用到了一個我沒見過的類VelocityTracker,郭神說這個類是用來計算手指滑動的速度,具體該怎么使用,我將在下一篇文章中進行說明。
之前提到的問題,就是設(shè)置滑動監(jiān)聽的View,如果該View不是ListView而是ImageView,TextView,LinearLayout,那么向右滑動的時候就會出現(xiàn)無法滑動的問題,大家可以自己試一下,我也沒找到解決的方法,所以如果大家找到了解決方法,希望能和我交流一下。
小結(jié)
終于把源碼看完了,還是佩服郭神的實力,代碼確實很驚艷,而且包括了很多的東西,自己看完并且弄懂之后對自己也是一種提高。希望看源碼之路能越走越遠!
源碼下載,點這里。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
android實現(xiàn)文本復(fù)制到剪切板功能(ClipboardManager)
Android也有剪切板(ClipboardManager),可以復(fù)制一些有用的文本到剪貼板,以便用戶可以粘貼的地方使用,下面是使用方法2014-02-02
Android ScrollView只能添加一個子控件問題解決方法
這篇文章主要介紹了Android ScrollView只能添加一個子控件問題解決方法,涉及Android界面布局的相關(guān)技巧,需要的朋友可以參考下2016-02-02
Android 5.0及以上編程實現(xiàn)屏幕截圖功能的方法
這篇文章主要介紹了Android 5.0及以上編程實現(xiàn)屏幕截圖功能的方法,結(jié)合實例形式分析了Android5.0以上實現(xiàn)截圖功能的相關(guān)類、函數(shù)及權(quán)限控制等操作技巧,需要的朋友可以參考下2018-01-01
為Android系統(tǒng)添加config.xml 新配置的設(shè)置
這篇文章主要介紹了為Android系統(tǒng)添加config.xml 新配置的設(shè)置,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android 動態(tài)改變SeekBar進度條顏色與滑塊顏色的實例代碼
在上次android開發(fā)的項目中遇到個這樣的需求,要動態(tài)改變seekbar進度條顏色與滑塊顏色的需求,實現(xiàn)代碼也算比較簡單,對實現(xiàn)過程感興趣的朋友可以通過本文學(xué)習(xí)下2016-11-11
Android存儲卡讀寫文件與Application數(shù)據(jù)保存的實現(xiàn)介紹
這篇文章主要介紹了Android在存儲卡上讀寫文件、Application保存數(shù)據(jù)的實現(xiàn)步驟,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-09-09

