Android 仿QQ側邊欄,自定義view的學習

轉載請註明出處:http://blog.csdn.net/lrs123123/article/details/41896677


恩,從一個神馬都不懂的東西小菜鳥到現在懂了點皮毛的碼界菜鳥,感慨頗多,不廢話,進入正題!


先上最終效果圖:




SlidingMenu大家都很熟悉對吧,但是平常可能大家都是直接導入第三方包,搞定,今天,我們自己動手豐衣足食。對於滑動view的移動,從整體佈局分析,我們可以有三種實現形式,SlidingDrawer,RelativeLayout,還有就是ViewGroup。對於這三種的分析,在這裏就不贅言了,我們先來學習viewGroup中的HorizontalScrollView,來作爲自定義空間的主體。


那麼我們的思路應該怎麼做呢,記住,羅馬不是一天建成的,我們要有簡單到複雜,不管多奇怪,多複雜的項目,他也是從helloworld變來的

那麼,我們先做出普通HorizontalScrollView效果,以唯品爲例如下:



先做出唯品會例子的效果吧,我們使用HorizontalScrollView,水平放置菜單和內容,HorizontalScrollView使用的好處就是,只需要監聽ACTION_UP就好了,它本身就帶有滑動的功能,廢話不多說,上代碼!


1、佈局文件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:garry="http://schemas.android.com/apk/res/com.Garry.myslidingmenudemoa"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.Garry.myslidingmenudemoa.MySlidingMenu
        android:id="@+id/msm"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/taylor"
        garry:RightPadding="150dp" >

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="horizontal" >

            <include layout="@layout/layout_menu" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@drawable/myqq" >
            </LinearLayout>
        </LinearLayout>
    </com.Garry.myslidingmenudemoa.MySlidingMenu>

</RelativeLayout>

一個RelativeLayout,裏邊放上我們的自定義View, 裏邊放水平LinearLayout,再放上Menu佈局,ok~

裏邊涉及到一點簡單的自定義屬性,我們需要新建attr.xml,在裏邊自定義我們要的屬性如下:

<resources>

    <attr name="RightPadding" format="dimension"></attr>

    <declare-styleable name="MySlidingMenu">
        <attr name="RightPadding"></attr>
    </declare-styleable>

</resources>

這樣,我們就能在空間裏邊使用我們自己定義的屬性 RightPadding了,
 garry:RightPadding="150dp" 


2、自定義控件

核心代碼~

1.MySlidingMenu

package com.Garry.myslidingmenudemoa;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;

import com.nineoldandroids.view.ViewHelper;

public class MySlidingMenu extends HorizontalScrollView implements
		OnGestureListener {

	private LinearLayout mWapper;
	private ViewGroup mMenu;
	private ViewGroup mContent;

	private int mScreenWidth; // 屏幕寬度
	private int mMenuWidth;

	private int mMenuRightPadding = 30;

	private boolean once;
	private boolean isOpen;

	private GestureDetector detector = new GestureDetector(this);

	/**
	 * 當使用自定義屬性時調用
	 * 
	 * @param context
	 * @param attrs
	 * @param defStyle
	 */
	public MySlidingMenu(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		// 獲取自定義屬性
		TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
				R.styleable.MySlidingMenu, defStyle, 0);
		int n = a.getIndexCount();
		for (int i = 0; i < n; i++) {
			int attr = a.getIndex(i);
			switch (attr) {
			case R.styleable.MySlidingMenu_RightPadding:
				mMenuRightPadding = a.getDimensionPixelOffset(attr,
						(int) TypedValue.applyDimension(
								TypedValue.COMPLEX_UNIT_DIP, 50, context
										.getResources().getDisplayMetrics()));
				break;
			}
		}
		// 獲取屏幕寬度
		WindowManager wm = (WindowManager) context
				.getSystemService(Context.WINDOW_SERVICE);
		DisplayMetrics outMetrics = new DisplayMetrics();
		wm.getDefaultDisplay().getMetrics(outMetrics);
		mScreenWidth = outMetrics.widthPixels;

		detector.setIsLongpressEnabled(true); // 這句話有多重要

	}

	public MySlidingMenu(Context context) {
		this(context, null); // 一個參數構造方法 去調用 兩個參數構造方法

	}

	/**
	 * 未使用自定義屬性時調用
	 * 
	 * @param context
	 * @param attrs
	 */
	public MySlidingMenu(Context context, AttributeSet attrs) {
		this(context, attrs, 0); // 兩個參數構造方法調用 三個參數構造方法

	}

	/**
	 * 觸摸操作 通過設置偏移量 將menu隱藏起來
	 */
	@Override
	public boolean onTouchEvent(MotionEvent ev) {

		detector.onTouchEvent(ev);
		return super.onTouchEvent(ev);
	}

	/**
	 * 設置子view的寬和高 設置自己的寬和高
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		if (!once) {
			mWapper = (LinearLayout) getChildAt(0);
			mMenu = (ViewGroup) mWapper.getChildAt(0);
			mContent = (ViewGroup) mWapper.getChildAt(1);
			mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth
					- mMenuRightPadding;
			mContent.getLayoutParams().width = mScreenWidth;
			once = true;
		}
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}

	/**
	 * 設置子view位置
	 * 
	 * @param changed
	 *            通過設置偏移量 將mMenu隱藏
	 */
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		if (changed) {
			smoothScrollTo(mMenuWidth, 0);
		}
	}

	@Override
	public boolean onDown(MotionEvent e) {
		Log.e("onDown", "YEAYEASYEAOJUOIUEIOAS");
		return false;
	}

	@Override
	public void onShowPress(MotionEvent e) {

	}

	@Override
	public boolean onSingleTapUp(MotionEvent e) {
		return false;
	}

	// 滑動
	@Override
	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
			float distanceY) {
		// Log.e("onScroll", "YEAYEASYEAOJUOIUEIOAS");
		return false;
	}

	// 長按
	@Override
	public void onLongPress(MotionEvent e) {
		Log.e("onLongPress", "YEAYEASYEAOJUOIUEIOAS");

	}

	// 按,滑動
	@Override
	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
			float velocityY) {
		Log.e("onFling", "YEAYEASYEAOJUOIUEIOAS");
		Log.e("VELVELVEL", "is" + velocityX); // 測試得知,當向左滑動,velocityX爲負,向右爲正
		if (velocityX > 10.0) {
			smoothScrollTo(-mMenuWidth,0);
		} else {
			smoothScrollTo(mMenuWidth, 0);
		}

		return false;
	}

}

要不是我把GestureDeletor,代碼量是很少的,不信? 對比下吧

package com.Garry.myslidingmenudemoa;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;

public class MySlidingMenu extends HorizontalScrollView {

	private LinearLayout mWapper;
	private ViewGroup mMenu;
	private ViewGroup mContent;

	private int mScreenWidth; // 屏幕寬度
	private int mMenuWidth;

	private int mMenuRightPadding = 30;

	private boolean once;
	private boolean isOpen;

	/**
	 * 當使用自定義屬性時調用
	 * 
	 * @param context
	 * @param attrs
	 * @param defStyle
	 */
	public MySlidingMenu(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		// 獲取自定義屬性
		TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
				R.styleable.MySlidingMenu, defStyle, 0);
		int n = a.getIndexCount();
		for (int i = 0; i < n; i++) {
			int attr = a.getIndex(i);
			switch (attr) {
			case R.styleable.MySlidingMenu_RightPadding:
				mMenuRightPadding = a.getDimensionPixelOffset(attr,
						(int) TypedValue.applyDimension(
								TypedValue.COMPLEX_UNIT_DIP, 50, context
										.getResources().getDisplayMetrics()));
				break;

			}
		}
		// 獲取屏幕寬度
		WindowManager wm = (WindowManager) context
				.getSystemService(Context.WINDOW_SERVICE);
		DisplayMetrics outMetrics = new DisplayMetrics();
		wm.getDefaultDisplay().getMetrics(outMetrics);
		mScreenWidth = outMetrics.widthPixels;

	}

	public MySlidingMenu(Context context) {
		this(context, null); // 一個參數構造方法 去調用 兩個參數構造方法

	}

	/**
	 * 未使用自定義屬性時調用
	 * 
	 * @param context
	 * @param attrs
	 */
	public MySlidingMenu(Context context, AttributeSet attrs) {
		this(context, attrs, 0); // 兩個參數構造方法調用 三個參數構造方法

	}

	/**
	 * 設置子view的寬和高 設置自己的寬和高
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		if (!once) {
			mWapper = (LinearLayout) getChildAt(0);
			mMenu = (ViewGroup) mWapper.getChildAt(0);
			mContent = (ViewGroup) mWapper.getChildAt(1);
			mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth
					- mMenuRightPadding;
			mContent.getLayoutParams().width = mScreenWidth;
			once = true;
		}
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}

	/**
	 * 設置子view位置
	 * 
	 * @param changed
	 *            通過設置偏移量 將mMenu隱藏
	 */
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		if (changed) {
			scrollTo(mMenuWidth, 0);
		}
	}

	/**
	 * 觸摸操作 通過設置偏移量 將menu隱藏起來
	 */
	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		int action = ev.getAction();
		switch (action) {
		case MotionEvent.ACTION_UP:
			int scrollX = getScrollX();
			if (scrollX >= mMenuWidth / 2) {
				this.smoothScrollTo(mMenuWidth, 0);
				isOpen = false;
			} else {
				this.smoothScrollTo(0, 0);
				isOpen = true;
			}
			return true;
		}

		return super.onTouchEvent(ev);
	}
}

好啦,簡單吧,就不解釋了,註釋說的挺仔細了


至此,已經基本可以實現唯品會那種簡單的策劃效果了,這時,我們回頭來看看和最終結果比較,還是差挺多的

1、最終結果可以縮小放大

2、最終側滑菜單有透明度的效果

3、進階:實現思路

恩,動畫效果,恩,選屬性動畫,那麼應該加在哪裏呢,ACTION_MOVE?可以,不過看了大神的博客,最終我們選在了流暢度搞得ScrollView裏的天然方法:ScrollChanged

@Override
	protected void onScrollChanged(int l, int t, int oldl, int oldt) {
		super.onScrollChanged(l, t, oldl, oldt);
		float scale = l * 1.0f / mMenuWidth;
		float leftScale = 1 - 0.3f * scale;
		float rightScale = 0.8f + scale * 0.2f;
		
		ViewHelper.setScaleX(mMenu, leftScale);
		ViewHelper.setScaleY(mMenu, leftScale);
		ViewHelper.setAlpha(mMenu, 0.2f+0.8f*(1 - scale));	//透明度設置
		
		ViewHelper.setPivotX(mContent, 0);
		ViewHelper.setPivotY(mContent, mContent.getHeight());
		ViewHelper.setScaleX(mContent, rightScale);
		ViewHelper.setScaleY(mContent,rightScale);
		
	}

恩,這部分主要參考大神的思路,直接貼吧  (以下爲摘錄)

我們在onScrollChanged裏面,拿到 l 也就是個getScrollX,即菜單已經顯示的寬度值;

與菜單的寬度做除法運算,在菜單隱藏到顯示整個過程,會得到1.0~0.0這麼個變化的區間;

有了這個區間,就可以根據這個區間設置動畫了;


1 首先是內容區域的縮放比例計算:

我們準備讓在菜單出現的過程中,讓內容區域從1.0~0.8進行變化~~

那麼怎麼把1.0~0.0轉化爲1.0~0.8呢,其實很簡單了:

float rightScale = 0.8f + scale * 0.2f; (scale 從1到0 )

接下來還有3個動畫:


2  菜單的縮放比例計算

菜單大概縮放變化是0.7~1.0

float leftScale = 1 - 0.3f * scale;


3 菜單的透明度比例:

我們設置爲0.2~1.0;即:0.2f + 0.8f * (1 - scale)


4 菜單的x方向偏移量:

看最終結果,並非完全從被內容區域覆蓋,還是有一點拖出的感覺,所以我們的偏移量這麼設置:

tranlateX = mMenuWidth * scale * 0.6f ;剛開始還是讓它隱藏一點



4、未解決部分


呼,對比QQ我們還需要在加手勢,接下來就有點頭疼,這搞了我很久很久,不過最終還是憋出來了

完整代碼最上邊有了,各位客官請往樓上挪位,使用GestureDeletor手勢類,如果要介紹估計又要多一篇博文來介紹了,等以後有時間再寫吧

有幾個要注意的點: 記得複寫onTouchEvent方法,在方做申明deletor.onTouchEvent(MotionEvent)   還有一個很多人剛開始用容易忽略的,記得要加上一句:detector.setIsLongpressEnabled(true); 你以爲這樣就好了麼?太天真了,還有個大坑等着你呢!


相信很多童鞋認真的寫完一運行就會發現,額,滑動沒效果!,怎麼回事?debug!,設置log,怎麼折騰也好,smoothScrollTo就是一直無效!爲什麼呢!


起先我也困擾很久,知道谷歌告訴我答案,不得不說,國外的大神還是比國內叼,加上這樣一段代碼後,奇蹟般的復活了!很簡單一段代碼


this.post(new Runnable() {
        @Override
	public void run() {
	smoothScrollTo(-mMenuWidth, 0);
				  }
});


見證奇蹟吧少年!記得兩處都要加哦,至於爲什麼,以我目前的知識,還不太說得清楚,附上解決的國外大神帖子:相關鏈接   想不通,因爲一般我們調用post,是在非ui線程調用UI線程的時候使用,而在我們這個Demo裏,我試過去獲取它的線程和mainActivity的線程,都是1,也就是說兩者都是UI線程,那爲什麼可以調用post呢?這也算是一個未解決的問題,希望各位看官有什麼思路多多提出來分享啊,好啦,仿QQ側邊欄也就和大家分享到這




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章