雖然QQ5.0已通過去好久了,可是有些特效仍是值得學習的java
效果:android
源碼點我git
導入的jar包,github
一個是高版本的support.v4包,須要這個v4包中有ViewDragHelper.app
我這裏使用的是support-v4:24.1.1dom
還要添加一個nineoldandroids的jar包,這是一個開源的動畫庫,使用方便。ide
GitHub地址:https://github.com/JakeWharton/NineOldAndroids佈局
項目源碼裏也有這些post
佈局主要分爲菜單界面和主界面學習
layout_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/my_layout" android:background="#ffffff" android:orientation="vertical" > <RelativeLayout android:layout_width="match_parent" android:layout_height="50dp" android:background="#18B4ED" > <ImageView android:layout_width="30dp" android:layout_marginLeft="15dp" android:id="@+id/iv_head" android:layout_height="30dp" android:layout_centerVertical="true" android:background="@drawable/head" /> </RelativeLayout> <ListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/main_listview" android:listSelector="@android:color/transparent"></ListView> </LinearLayout>
預覽效果:
layout/layout_menu.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingLeft="20dp" android:paddingTop="50dp" > <ImageView android:layout_width="50dp" android:layout_height="50dp" android:src="@drawable/head" /> <ListView android:id="@+id/menu_listview" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="5dp" android:listSelector="@android:color/transparent" > </ListView> </LinearLayout>
預覽
最後activity_main.xml
這裏設置背景圖片
<com.example.xw.qqslidemenu.SlideMenu 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" android:id="@+id/slideMenu" android:background="@drawable/bg" > <!-- 菜單的佈局 --> <include layout="@layout/layout_menu"/> <!-- 主界面的佈局 --> <include layout="@layout/layout_main"/> </com.example.xw.qqslidemenu.SlideMenu>
接下來開始寫咱們的自定義SlideMenu
1.繼承FrameLayout,這裏利用FrameLayout的特性,方便
2.實現Drag拖拉跟隨即動畫,縮放等
3.onTouchEvent的事件處理
首先上代碼,再來分析
import android.content.Context; import android.support.v4.graphics.ColorUtils; import android.support.v4.view.ViewCompat; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; import com.nineoldandroids.animation.FloatEvaluator; import com.nineoldandroids.animation.IntEvaluator; import com.nineoldandroids.view.ViewHelper; import java.net.InterfaceAddress; /** * Created by xw on 2016/8/21. */ public class SlideMenu extends FrameLayout { private View menuView;//菜單的view private View mainView;//主界面的view private ViewDragHelper viewDragHelper; private int width; private float dragRange;//拖拽範圍 private FloatEvaluator floatEvaluator;//float的計算器 private IntEvaluator intEvaluator;//int的計算器 private OnDragStateChangeListener listener;//回調監聽器 //定義狀態常量 enum DragState{ Open,Close; } private DragState currentState = DragState.Close;//當前SlideMenu的狀態默認是關閉的 public SlideMenu(Context context) { super(context); init(); } public SlideMenu(Context context, AttributeSet attrs) { super(context, attrs); init(); } public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { viewDragHelper=ViewDragHelper.create(this,callback); floatEvaluator = new FloatEvaluator(); intEvaluator = new IntEvaluator(); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return viewDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { viewDragHelper.processTouchEvent(event); return true; } private ViewDragHelper.Callback callback=new ViewDragHelper.Callback() { /** *用於判斷是否捕獲當前child的觸摸事件 * chid:當前觸摸的子View * return true:捕獲並處理 false:不處理 */ @Override public boolean tryCaptureView(View child, int pointerId) { return child==menuView||child==mainView; } /** * 獲取view水平方向的拖拽範圍,可是目前不能限制邊界 * 返回值目前用在手指擡起的時候view緩慢移動的動畫世界的計算上面 * 最好不能返回0 * @param child * @return */ @Override public int getViewHorizontalDragRange(View child) { return (int) dragRange; } /** * 控制child在水平方向的移動 left: * @param child * @param left 表示你想讓child的left改變的值,left=child.getLeft+dx * @param dx 本次移動距離 * @return 表示你真正想讓child的left變成的值 */ @Override public int clampViewPositionHorizontal(View child, int left, int dx) { if(child==mainView){ if (left<0) left=0; //限制左邊 if(left>dragRange) left= (int) dragRange; //限制右邊 } return left; } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { if(changedView==menuView){ //固定住menuView menuView.layout(0,0,menuView.getMeasuredWidth(),menuView.getMeasuredHeight()); //讓mainView移動起來 int newLeft=mainView.getLeft()+dx; if(newLeft<0) newLeft=0; if(newLeft>dragRange) newLeft= (int) dragRange; mainView.layout(newLeft,mainView.getTop()+dy,newLeft+mainView.getMeasuredWidth(),mainView.getBottom()+dy); } //1,計算滑動的百分比 float fraction =mainView.getLeft()/dragRange; //2執行伴隨動畫 executeAnim(fraction); if(fraction==0 && currentState!=DragState.Close){ //更改狀態爲關閉,並回調關閉的方法 currentState = DragState.Close; if(listener!=null)listener.onClose(); }else if (fraction==1f && currentState!=DragState.Open) { //更改狀態爲打開,並回調打開的方法 currentState = DragState.Open; if(listener!=null)listener.onOpen(); } //將drag的fraction暴漏給外界 if(listener!=null){ listener.onDraging(fraction); } } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { if(mainView.getLeft()<dragRange/2){ close(); } else{ open(); } //處理用戶的稍微滑動 if(xvel>200 && currentState!=DragState.Open){ open(); }else if (xvel<-200 && currentState!=DragState.Close) { close(); } } }; private void open() { viewDragHelper.smoothSlideViewTo(mainView, (int) dragRange,0); ViewCompat.postInvalidateOnAnimation(SlideMenu.this); } private void close() { viewDragHelper.smoothSlideViewTo(mainView,0,0); ViewCompat.postInvalidateOnAnimation(SlideMenu.this); } public void executeAnim(float fraction) { //縮小mainView ViewHelper.setScaleX(mainView,floatEvaluator.evaluate(fraction,1f,0.8f)); ViewHelper.setScaleY(mainView,floatEvaluator.evaluate(fraction,1f,0.8f)); //移動menuView ViewHelper.setTranslationX(menuView,intEvaluator.evaluate(fraction,-menuView.getMeasuredWidth()/2,0)); //放大menuView ViewHelper.setScaleX(menuView,floatEvaluator.evaluate(fraction,0.5f,1f)); ViewHelper.setScaleY(menuView,floatEvaluator.evaluate(fraction,0.5f,1f)); //改變menuView的透明度 ViewHelper.setAlpha(menuView,floatEvaluator.evaluate(fraction,0.3f,1f)); } public void computeScroll(){ if(viewDragHelper.continueSettling(true)){ ViewCompat.postInvalidateOnAnimation(SlideMenu.this); } } @Override protected void onFinishInflate() { super.onFinishInflate(); if(getChildCount()!=2){ throw new IllegalArgumentException("Slide should have two children"); } menuView=getChildAt(0); mainView=getChildAt(1); } /** * 該方法在onMeasure()方法執行完成以後完成, * 能夠在該方法初始化本身的寬高和子View的寬高 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width=getMeasuredWidth(); dragRange=width*0.6f; } public void setOnDragStateChangeListener(OnDragStateChangeListener listener){ this.listener = listener; } interface OnDragStateChangeListener{ void onOpen(); void onClose(); void onDraging(Float fraction); } }
第一步:
@Override protected void onFinishInflate() { super.onFinishInflate(); if(getChildCount()!=2){ throw new IllegalArgumentException("Slide should have two children"); } menuView=getChildAt(0); mainView=getChildAt(1); } /** * 該方法在onMeasure()方法執行完成以後完成, * 能夠在該方法初始化本身的寬高和子View的寬高 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width=getMeasuredWidth(); dragRange=width*0.6f; }
獲得兩個子View,和獲得width和拖動範圍dragRange
第二步
初始化ViewDragHelper
private void init(){ viewDragHelper = ViewDragHelper.create(this, callback); floatEvaluator = new FloatEvaluator(); intEvaluator = new IntEvaluator(); }
寫ViewDragHelper.Callback callback
按照順序複寫方法:
1.捕獲menuView或者mainView
/** * 用於判斷是否捕獲當前child的觸摸事件 * child: 當前觸摸的子View * return: true:就捕獲並解析 false:不處理 */ @Override public boolean tryCaptureView(View child, int pointerId) { return child==menuView || child==mainView; }
2.設置水平方向移動範圍
public int getViewHorizontalDragRange(View child) { return (int) dragRange; }
3.設置移動方向和限制邊界
/** * 控制child在水平方向的移動 left: * 表示ViewDragHelper認爲你想讓當前child的left改變的值,left=chile.getLeft()+dx dx: * 本次child水平方向移動的距離 return: 表示你真正想讓child的left變成的值 */ public int clampViewPositionHorizontal(View child, int left, int dx) { if(child==mainView){ if(left<0)left=0;//限制mainView的左邊 if(left>dragRange)left=(int) dragRange;//限制mainView的右邊 } return left; }
4,實現伴隨移動和根據移動百分比執行動畫,關於接口回調方法和執行動畫代碼參考一開始的全代碼
@Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { if(changedView==menuView){ //固定住menuView menuView.layout(0,0,menuView.getMeasuredWidth(),menuView.getMeasuredHeight()); //讓mainView移動起來 int newLeft=mainView.getLeft()+dx; if(newLeft<0) newLeft=0; if(newLeft>dragRange) newLeft= (int) dragRange; mainView.layout(newLeft,mainView.getTop()+dy,newLeft+mainView.getMeasuredWidth(),mainView.getBottom()+dy); } //1,計算滑動的百分比 float fraction =mainView.getLeft()/dragRange; //2執行伴隨動畫 executeAnim(fraction); if(fraction==0 && currentState!=DragState.Close){ //更改狀態爲關閉,並回調關閉的方法 currentState = DragState.Close; if(listener!=null)listener.onClose(); }else if (fraction==1f && currentState!=DragState.Open) { //更改狀態爲打開,並回調打開的方法 currentState = DragState.Open; if(listener!=null)listener.onOpen(); } //將drag的fraction暴漏給外界 if(listener!=null){ listener.onDraging(fraction); } }
5.釋放時根據位置或者滑動加速度控制菜單是打開仍是關閉
@Override public void onViewReleased(View releasedChild, float xvel, float yvel) { if(mainView.getLeft()<dragRange/2){ close(); } else{ open(); } //處理用戶的稍微滑動 if(xvel>200 && currentState!=DragState.Open){ open(); }else if (xvel<-200 && currentState!=DragState.Close) { close(); } }
6最後別忘了攔截Touch事件
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { return viewDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { viewDragHelper.processTouchEvent(event); return true; }
7.
public void computeScroll(){ if(viewDragHelper.continueSettling(true)){ ViewCompat.postInvalidateOnAnimation(SlideMenu.this); } }
最後加上回調接口,用枚舉表示狀態
最後給listview填充數據,及實現imageView的一些特效
MainActivity
import android.graphics.Color; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.view.animation.CycleInterpolator; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.nineoldandroids.view.ViewHelper; import com.nineoldandroids.view.ViewPropertyAnimator; import java.util.Random; public class MainActivity extends AppCompatActivity { private ListView mainlv; private ListView menulv; private SlideMenu slideMenu; private ImageView iv_head; public static final String[] sCheeseStrings = { "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi", "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale", "Aisy Cendre", "Allgauer Emmentaler", "Alverca", "Ambert", "American Cheese", "Ami du Chambertin", "Anejo Enchilado", "Anneau du Vic-Bilh", "Anthoriro", "Appenzell", "Aragon", "Ardi Gasna", "Ardrahan", "Armenian String", "Aromes au Gene de Marc", "Cougar Gold", "Coulommiers", "Coverdale", "Crayeux de Roncq", "Cream Cheese", "Cream Havarti", "Crema Agria", "Crema Mexicana", "Creme Fraiche", "Crescenza", "Croghan", "Crottin de Chavignol", "Crottin du Chavignol", "Crowdie", "Crowley", "Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano" }; public static final String[] NAMES = new String[] { "宋江", "盧俊義", "吳用", "公孫勝", "關勝", "林沖", "秦明", "呼延灼", "花榮", "柴進", "李應", "朱仝", "魯智深", "武松", "董平", "張清", "楊志", "徐寧", "索超", "戴宗", "劉唐", "李逵", "史進", "穆弘", "雷橫", "李俊", "阮小二", "張橫", "阮小五", " 張順", "阮小七", "楊雄", "石秀", "解珍", " 解寶", "燕青", "朱武", "黃信", "孫立", "宣贊", "郝思文", "韓滔", "彭玘", "單廷珪", "魏定國", "蕭讓", "裴宣", "歐鵬", "鄧飛", " 燕順", "楊林", "凌振", "蔣敬", "呂方", "郭 盛", "安道全", "皇甫端", "王英", "扈三娘", "鮑旭", "樊瑞", "孔明", "孔亮", "項充", "李袞", "金大堅", "馬麟", "童威", "童猛", "孟康", "侯健", "陳達", "楊春", "鄭天壽", "陶宗旺", "宋清", "樂和", "龔旺", "丁得孫", "穆春", "曹正", "宋萬", "杜遷", "薛永", "施恩", }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); iv_head= (ImageView) findViewById(R.id.iv_head); mainlv= (ListView) findViewById(R.id.main_listview); menulv= (ListView) findViewById(R.id.menu_listview); mainlv.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,NAMES)); menulv.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,sCheeseStrings){ @Override //改變textView顏色 public View getView(int position, View convertView, ViewGroup parent) { TextView textView = (TextView) super.getView(position, convertView, parent); textView.setTextColor(Color.WHITE); return textView; } }); slideMenu= (SlideMenu) findViewById(R.id.slideMenu); slideMenu.setOnDragStateChangeListener(new SlideMenu.OnDragStateChangeListener() { @Override public void onOpen() { menulv.smoothScrollToPosition(new Random().nextInt(menulv.getCount())); } @Override public void onClose() { ViewPropertyAnimator.animate(iv_head).translationXBy(15) .setInterpolator(new CycleInterpolator(4)) .setDuration(500) .start(); } @Override public void onDraging(Float fraction) { ViewHelper.setAlpha(iv_head,1-fraction); } }); } }
效果
觀察效果的同時,咱們也發現了BUG,放咱們切換到菜單界面時,咱們main_view的listview仍是能夠滑動的,這確定是不符合指望的,
咱們應該在菜單打開時,禁止main_view的listview滑動。
如何去禁止listviiew滑動?listview的父親LinearLayout攔截並消費滑動事件!
根據什麼判斷是否攔截?根據SlideMenu的狀態,是打開仍是關閉。
因此要實現一個自定義的LinearLayout
在這以前,須要SlideMenu提供一個暴露狀態的方法
添加方法
/** * 獲取當前的狀態 * @return */ public DragState getCurrentState(){ return currentState; }
MyLinearLayout
package com.example.xw.qqslidemenu; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.LinearLayout; /** * 當slideMenu打開的時候,攔截並消費掉觸摸事件 * */ public class MyLinearLayout extends LinearLayout { public MyLinearLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public MyLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } public MyLinearLayout(Context context) { super(context); } private SlideMenu slideMenu; public void setSlideMenu(SlideMenu slideMenu){ this.slideMenu = slideMenu; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if(slideMenu!=null && slideMenu.getCurrentState()== SlideMenu.DragState.Open){ //若是slideMenu打開則應該攔截並消費掉事件 return true; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { if(slideMenu!=null && slideMenu.getCurrentState()== SlideMenu.DragState.Open){ //若是slideMenu打開則應該攔截並消費掉事件 return true; } return super.onTouchEvent(event); } }
最後將layout_main的最外層修改成咱們的自定義LinearLayout
在MianActivity中根據id找到MyLinearLayout,設置setSlideMenu傳入咱們的SlideMenu當參數。
完成。