ViewDragHelper的介紹
要實現和QQ5.0側滑的特效,需要藉助谷歌在2013年I/O大會上發佈的ViewDragHelper類,提供這個類目的就是爲了解決拖拽滑動問題。
使用v4包中的ViewDragHelper爲了兼容低版本,所以在創建ViewDragHelper對象時如果找不到ViewDragHelper這個類,可以從sdk中拷貝出最新的v4包覆蓋lib目錄中的V4包即可。
- 效果圖:
/**
* Created by Alan on 2016/8/18.
* QQ5.0側滑菜單特效實現
*/
//自定義一個類繼承FrameLayout
public class DragLayout extends FrameLayout {
/**
* ViewDragHelper:
* v4包中的api,2013年穀歌I/O大會上提出,用於解決控件拖動問題。
*/
private ViewDragHelper mDragHelper;
private View mMenuView;//菜單
private View mMainView;//主界面
private int mWidth;//控件寬度
private int mHeight;///控件高度
private int mMRange;//側拉菜單打開的最大寬度
public DragLayout(Context context) {
super(context);
init();
}
public DragLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//4.處理Callback接口中的回調方法
ViewDragHelper.Callback mCallack = new ViewDragHelper.Callback() {
/**
* 根據返回值決定子控件是否可以拖動, true表示可以拖動
* @param child 被捕獲的子控件
* @param pointerId
* @return
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
/**當開始拖動時回調*/
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
System.out.println("onViewCaptured -- 拖動");
}
/**
* 根據返回值決定子控件將要顯示的位置
* @param child
* @param left
* @param dx
* @return
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if (child == mMainView) {
left = fixDragLeft(left);
}
return left;
}
/**
* 獲取水平方向拖動的最大範圍
* 注意:此方法半不會真正限制子控件的滑動範圍,
* 如果要實現拖動效果,應該返回大於0的值
* @param child
* @return mMRange 範圍
*/
@Override
public int getViewHorizontalDragRange(View child) {
return mMRange;
}
/**
* 當拖動結束鬆開手時回調, 需要在此方法中設置菜單爲打開或關閉狀態
* 側滑菜單何時打開和關閉?
* 打開的情況:
* 1) 滑動速度大於0
* 2) 速度是0且位置大於拖拽範圍的一半
* 關閉的情況:其他的情況都是關閉
*
* @param releasedChild
* @param xvel 鬆開手時水平方向的速度,像素/秒,往右爲正
* @param yvel
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
// 鬆開手時打開或關閉側滑菜單
if (xvel > 0) {
open();
} else if (xvel == 0 && mMainView.getLeft() > mMRange / 2) {
open();
} else {
close();
}
}
/**
* 當前界面發生改變回調
* 處理邏輯: 關聯滑動, 事件監聽, 伴隨動畫
* @param changedView
* @param left
* @param top
* @param dx
* @param dy
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
// super.onViewPositionChanged(changedView, left, top, dx, dy);
// 當滑動菜單時,同時滑動主界面
if (changedView == mMenuView) {
// 保持菜單界面位置不變
mMenuView.layout(0, 0, mWidth, mHeight);
// 關聯滑動,菜單滑動多少,主界面也滑動多少
int newLeft = mMainView.getLeft() + dx;
newLeft = fixDragLeft(newLeft);
mMainView.layout(newLeft, 0, newLeft + mWidth, mHeight);
}
// 監聽打開關閉拖動的狀態
listenDragState();
// 伴隨動畫
animateChildren();
}
};
/**
* 伴隨動畫
*/
private void animateChildren() {
float percent = ((float) mMainView.getLeft()) / mMRange;
//菜單界面
// 平移: [-mWidth / 2, 0]
// 透明度變化: [0.4, 1]
// 縮放: [0.5f, 1]
mMenuView.setTranslationX(evaluate(-mWidth/2,0,percent));
mMenuView.setAlpha(evaluate(0.4f, 1, percent));
mMenuView.setScaleX(evaluate(0.5f, 1f, percent));
mMenuView.setScaleY(evaluate(0.5f, 1f, percent));
// 主界面: 縮放[1, 0.8f]
mMainView.setScaleX(evaluate(1f, 0.8f, percent));
mMainView.setScaleY(evaluate(1f, 0.8f, percent));
Drawable drawable = getBackground();
if (drawable != null) {
int color = (int) evaluateColor(percent, Color.BLACK, Color.TRANSPARENT);
drawable.setColorFilter(color , PorterDuff.Mode.SRC_OVER);
}
}
/**
* 估值器
*/
public float evaluate(float start,float end,float percent){
// 中間值 = 開始值 + (結束值 - 開始值) * 百分比
return start+(end - start)*percent;
}
public Object evaluateColor(float fraction, Object startValue, Object endValue) {
int startInt = (Integer) startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = (Integer) endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
(int)((startR + (int)(fraction * (endR - startR))) << 16) |
(int)((startG + (int)(fraction * (endG - startG))) << 8) |
(int)((startB + (int)(fraction * (endB - startB))));
}
//------------側滑菜單事件監聽------以下------------
/**
* 事件監聽(打開,關閉,拖動)
*/
private void listenDragState() {
int left = mMainView.getLeft();
if(left == 0){
mCurrentStatus = DragStatus.CLOSE;//關閉
}else if(left == mMRange){
mCurrentStatus = DragStatus.OPEN;//打開
}else{
mCurrentStatus = DragStatus.DRAGGING;//拖動
}
//當事件發生時,回調監聽器中的方法
if(mOnDragListener != null){
if(mCurrentStatus == DragStatus.OPEN){
//回調監聽器中相應的方法
}else if(mCurrentStatus == DragStatus.CLOSE){
mOnDragListener.onClose();
}else{
float v = ((float) mMainView.getLeft()) / mMRange;
mOnDragListener.onDraging(v);
}
}
}
public boolean isOpen() {
return mCurrentStatus == DragStatus.OPEN;
}
//定義一個枚舉
public enum DragStatus{
OPEN,CLOSE,DRAGGING
}
private DragStatus mCurrentStatus = DragStatus.CLOSE;//默認狀態
/**
* 提供狀態獲取
* @return
*/
public DragStatus getCurrentStatus() {
return mCurrentStatus;
}
/**
* 1.自定義的監聽器
*/
private OnDragListener mOnDragListener;
public interface OnDragListener {
void onOpen();
void onClose();
void onDraging(float value);
}
/**
* 給外部提供設置監聽器的方法
* @param onDragListener
*/
public void setOnDragListener(OnDragListener onDragListener){
mOnDragListener = onDragListener;
}
//--------------事件監聽-----以上---------------------------
private int fixDragLeft(int left) {
if (left < 0) {
left = 0;
} else if (left > mMRange) {
left = mMRange;
}
return left;
}
/**
* 關閉菜單
*/
private void close() {
mMenuView.layout(0, 0, mWidth, mHeight);
// mMainView.layout(0,0,mWidth,mHeight);
//主界面平滑滾動
mDragHelper.smoothSlideViewTo(mMainView, 0, 0);
//刷新 調用順序Invalidate --> onDraw() --> computeScroll(重寫,在此方法裏面需要繼續刷新)
ViewCompat.postInvalidateOnAnimation(this);
}
/**
* 打開菜單
*/
private void open() {
mMenuView.layout(0, 0, mWidth, mHeight);
//mMainView.layout(mMRange,0,mWidth+mMRange,mHeight);
// 平滑滾動到某一位置: 第一步
mDragHelper.smoothSlideViewTo(mMainView, mMRange, 0);
//刷新 調用順序Invalidate --> onDraw() --> computeScroll(重寫,在此方法裏面需要繼續刷新)
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public void computeScroll() {
super.computeScroll();
//第二步
//如果沒有滑動到指定位置,需要繼續刷新
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private void init() {
//1.創建ViewDragHelper對象
float sensitivity = 1.0f;//敏感值,數值越大則越容易滑動菜單
mDragHelper = ViewDragHelper.create(this, sensitivity, mCallack);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 2. 讓ViewDragHelper決定是否攔截事件
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 3. 把事件交給ViewDragHelper處理
mDragHelper.processTouchEvent(event);
//按下時需要返回true,否則無法接收到後續的move和up事件
if (event.getAction() == MotionEvent.ACTION_DOWN) {
return true;
}
return super.onTouchEvent(event);
}
/**
* 當填充結束後回調此方法,注意:不能在此方法獲取控件的寬高
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// 健壯性處理
if (getChildCount() < 2) {
throw new IllegalStateException("DragLayout至少要有兩個子控件");
}
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
/**
* 執行此方法時,已經調用完了onMeasure,所以可以獲取控件寬高
*
* @param w
* @param h
* @param oldw
* @param oldh
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//獲取控件的寬高
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
//設置主界面只能滑到 %60;
mMRange = (int) (mWidth * 0.6f);
}
}
頭像左右晃動的實現
監聽當側滑面板關閉時通過屬性動畫實現左右晃動的動畫。
mDragLayout.setOnDragListener(new OnDragListener() {
@Override
public void open() {
}
@Override
public void onDragging(float percent) {
iv_header.setAlpha(1 - percent); // 拖動時設置透明度
}
@Override
public void close() {
showToast("close");
TranslateAnimation animation = new TranslateAnimation(0, 10, 0, 0);
animation.setDuration(100);
animation.setRepeatCount(4);
iv_header.startAnimation(animation);
}
});
問題解決:
處理菜單打開後主界面列表仍可滑動的問題
自定義主界面根佈局控件,並重寫攔截方法,當側滑菜單打開時返回true攔截事件:
public class MyLinearLayout extends LinearLayout {
private DragLayout dragLayout;
public MyLinearLayout(Context context) {
super(context);
}
public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setDragLayout(DragLayout dragLayout) {
this.dragLayout = dragLayout;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 如果側菜單打開,則攔截事件,不讓列表點擊或者滾動
if (dragLayout.isOpen()) {
return true;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//如果菜單打開,不允許觸摸主界面時,菜單列表滑動
if(layout!= null && layout.isOpen()){
return true;
}
return super.onTouchEvent(event);
}
}
完整源碼:點擊下載