DragLayout: QQ5.0侧拉菜单的新特效
2016-11-14 11:30 阅读(221)

一、项目概要

  1.1 项目效果如图:

  gif

  

  1.2 需要使用到的技术

     ViewDragHelper: 要实现和QQ5.0侧滑的特效,需要借助谷歌在2013I/O大会上发布的ViewDragHelper类,提供这个类目的就是为了解决拖拽滑动问题

 

  1.3 侧滑菜单的实现方式

    1. SlidingMenu 第三方库

    2. DrawerLayout v4包中的类

    3. 自定义控件 

 

  1.4 一些回调方法 

    - tryCaptureView: 用来决定是否可以拖动
    - clampViewPositionHorizontal: 用来设置子控件将要显示的位置 [限制子控件拖动的范围]
    - getViewHorizontalDragRange:返回水平方向拖动的最大范围,返回大于0的值才可以拖动 
    - onViewPositionChanged: 位置改变时调用 [关联菜单与主界面的滑动,监听拖动状态,伴随动画]
    - onViewReleased: 拖动结束后,松开手时调用 [平滑地打开或关闭侧滑菜单]

 

二、项目实现

2.1 创建DragLayout

public class DragLayout extends FrameLayout {
    public DragLayout(Context context) {
        super(context);
    }
    public DragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

2.2 创建侧滑面板布局

<com.xiaowu.draglayout.view.DragLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drag_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/bg" >

    <!-- 侧滑菜单布局 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#33ff0000" />

    <!-- 主界面布局 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#3300ff00" />

</com.xiaowu.draglayout.view.DragLayout>

2.3 DragLayout的主程序代码,下面代码中有详细的讲解,我就不多分步骤实现了

package com.xiaowu.draglayout.view;

import android.content.Context;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
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;

/**
 * Created by ${VINCENT} on 2016/11/8.
 */

public class DragLayout extends FrameLayout {

    private ViewDragHelper mViewDragHelper;
    private View mMenuView;
    private View mMainView;
    private int mRange;
    private int mWidth;
    private int mHeight;

    public DragLayout(Context context) {
        super(context);
        init();
    }

    public DragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /** 填充完成后调用此方法 */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // 健壮性判断
        if (getChildCount() < 2) {
            throw new IllegalStateException("DrawLayout至少要有两个子控件");
        }
        mMenuView = getChildAt(0);
        mMainView = getChildAt(1);
    }

    // step1:创建ViewDragHelper对象
    private void init() {
        float sensitivity = 1.0f; //值越大,灵敏度越高
        mViewDragHelper = ViewDragHelper.create(this, sensitivity, mCallBack);
    }

    // step2:由ViewDragHelper决定是否拦截事件
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    // step3:把触摸事件交给ViewDragHelper处理
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mViewDragHelper.processTouchEvent(event);
        return true; //让mViewDragHelper持续接收到触摸事件
    }

    // step4:处理ViewDragHelper的Callback方法
    ViewDragHelper.Callback mCallBack = new ViewDragHelper.Callback() {

        // (1)捕获子控件,返回true表示子控件可以拖动
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return true;
        }

        // (2)子控件显示的方向(horizontal, vertical)
        // left: 被拖动控件的将要显示的位置
        // dx: 位置的偏移量 = left - 当前的left
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            if (child == mMainView) {
                left = reviseLeft(left);
            }
            return left;
        }

        // (3)返回水平方向拖动的最大范围mRange,内部会根据返回值计算动画执行的时间
        @Override
        public int getViewHorizontalDragRange(View child) {
            return mRange;
        }

        // (4)位置发生改变的回调
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            // (a) 关联子控件的滑动
            if (changedView == mMenuView) {
                // 侧拉菜单界面不变时
                mMenuView.layout(0, 0, mWidth, mHeight);
                // 主菜单界面的新位置
                int newLeft = mMenuView.getLeft() + dx;
                newLeft = reviseLeft(newLeft);
                mMainView.layout(newLeft, 0, mWidth + newLeft, mHeight);
            }
            // (b) 事件的监听(打开,拖动,关闭)
            listenDragStatus();
            // (c) 事件伴随的动画
            animateChildren();
        }

        // (5) 拖动结束时回调的方法
        // xvel:释放时的回调速度,在这里向右为正
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            if (xvel > 0) {
                open();
            } else if (xvel == 0 && mMainView.getLeft() > mRange / 2) {
                open();
            } else {
                close();
            }
        }
    };

    //============================动画的定义=====================================
    /** 估值器:变化值 = 开始值 + (结束值 - 开始值) * 百分比 */
    public float evaluate(float start, float end, float percent) {
        return start + (end - start) * percent;
    }

    protected void animateChildren() {
        float percent = ((float) mMainView.getLeft()) / mRange;

        // 1.主界面的缩放
        mMainView.setScaleX(evaluate(1f, 0.8f, percent));
        mMainView.setScaleY(evaluate(1f, 0.8f, percent));
        // 2.侧拉菜单的缩放
        mMenuView.setTranslationX((int) evaluate(-mRange, 0, percent)); // 平移
        mMenuView.setScaleX(evaluate(0.5f, 1.0f, percent));
        mMenuView.setScaleY(evaluate(0.5f, 1.0f, percent));
        mMenuView.setAlpha(evaluate(0.5f, 1.0f, percent));
        // 3.背景图片:亮度的变化
        Drawable background = getBackground();
        if (background != null) {
            // 过渡的颜色
            int color = (int)evaluate2(percent, Color.BLACK, Color.TRANSPARENT);
            background.setColorFilter(color, PorterDuff.Mode.SRC_OVER);
        }
    }

    /** 处理颜色渐变的兼容性问题 */
    public Object evaluate2(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 ((startA + (int)(fraction * (endA - startA))) << 24) |
                ((startR + (int)(fraction * (endR - startR))) << 16) |
                ((startG + (int)(fraction * (endG - startG))) << 8) |
                ((startB + (int)(fraction * (endB - startB))));
    }

    //============================状态的监听begin================================
    /** 事件的监听 */
    protected void listenDragStatus() {
        int left = mMainView.getLeft();
        if (left == 0) {
            mCurrentStatus = DragStatus.CLOSE;
        } else if (left == mRange) {
            mCurrentStatus = DragStatus.OPEN;
        } else {
            mCurrentStatus = DragStatus.DRAGGING;
        }

        //当事件发生时,调用监听器中的方法
        if (mOnDragListener != null) {
            if (mCurrentStatus == DragStatus.OPEN) {
                mOnDragListener.onOpen();
            } else if (mCurrentStatus == DragStatus.CLOSE) {
                mOnDragListener.onClose();
            } else {
                float percent = ((float) mMainView.getLeft()) / mRange;
                mOnDragListener.onDragging(percent);
            }
        }
    }

    /** 状态的定义 */
    public enum DragStatus {
        OPEN, CLOSE, DRAGGING
    }

    /** 当前的状态 */
    private DragStatus mCurrentStatus = DragStatus.CLOSE;

    public DragStatus getCurrentStatus() {
        return mCurrentStatus;
    }

    /** 定义接口 */
    public interface OnDragListener {
        void onOpen();
        void onClose();
        void onDragging(float percent);
    }

    private OnDragListener mOnDragListener;

    /** 提供设置监听器的set方法 */
    public void setOnDragListener(OnDragListener onDragListener) {
        this.mOnDragListener = onDragListener;
    }

    //============================状态的监听end================================

    @Override
    public void computeScroll() {
        super.computeScroll();
        // 若如果没有移动到正确的位置,需要刷新
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    /** 限定主界面的滑动范围 */
    protected int reviseLeft(int left) {
        if (left < 0) {
            left = 0;
        } else if (left > mRange) {
            left = mRange;
        }
        return left;
    }

    /** 控件尺寸发生改变时,回调该方法 */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 获取DrawLayout的宽高
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        // 拖拽的比例
        mRange = (int) (mWidth * 0.6f);
    }

    /** 打开侧拉菜单 */
    protected void open() {
        mViewDragHelper.smoothSlideViewTo(mMainView, mRange, 0);
        // 刷新界面
        ViewCompat.postInvalidateOnAnimation(this);
    }

    /** 关闭侧拉菜单 */
    protected void close() {
        mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
        // 刷新界面
        ViewCompat.postInvalidateOnAnimation(this);
    }

    /** 侧滑菜单是否打开 */
    public boolean isOpen() {
        return mCurrentStatus == DragStatus.OPEN;
    }

}

2.4 创建MyLinearLayout.java文件,处理侧拉与主菜单的冲突事件

package com.xiaowu.draglayout.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;

/**
 * Created by ${VINCENT} on 2016/11/9.
 */

public class MyLinearLayout extends LinearLayout {

    private DragLayout mDragLayout;

    public MyLinearLayout(Context context) {
        super(context);
    }

    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /** 根据它的打开状态决定是否要拦截事件 */
    public void setDragLayout(DragLayout dragLayout) {
        this.mDragLayout = dragLayout;
    }

    /** 如果侧滑菜单打开了,禁止主菜单的列表滑动 */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mDragLayout.isOpen()) {
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

    /** 如果侧滑菜单打开了,消费主菜单的触摸事件,禁止通过滑动主菜单使侧拉菜单的列表滑动 */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mDragLayout.isOpen()) {
            return true;
        }
        return super.onTouchEvent(event);
    }
}

 2.5 接下来是MainActivity的代码实现

package com.xiaowu.draglayout;

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.Window;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.xiaowu.draglayout.view.DragLayout;
import com.xiaowu.draglayout.view.MyLinearLayout;

public class MainActivity extends AppCompatActivity {

    private ImageView mIvHeader;
    private MyLinearLayout mMyLinearLayout;
    private DragLayout mDragLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);

        setContentView(R.layout.activity_main);
        mIvHeader = (ImageView) findViewById(R.id.iv_header);

        initDragLayout();
        mMyLinearLayout = (MyLinearLayout) findViewById(R.id.my_ll);
        // 根据打开的状态决定是否拦截事件
        mMyLinearLayout.setDragLayout(mDragLayout);

        initListView();
    }

    private void initListView() {
        ListView lvMenu = (ListView) findViewById(R.id.lv_menu);
        ListView lvMain = (ListView) findViewById(R.id.lv_main);

        lvMenu.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
                Constant.MENUS){
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                TextView view = (TextView) super.getView(position, convertView, parent);
                view.setTextSize(dp2px(16));
                view.setTextColor(Color.WHITE);
                return view;
            }
        });

        lvMain.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
                Constant.LIST_DATAS));

    }

    private void initDragLayout() {
        mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
        mDragLayout.setOnDragListener(new DragLayout.OnDragListener() {
            @Override
            public void onOpen() {
                showToast("打开");
            }

            @Override
            public void onClose() {
                showToast("关闭");
            }

            @Override
            public void onDragging(float percent) {
                mIvHeader.setAlpha(1 - percent );
            }
        });
    }

    /** toast使用单例模式,可以随状态刷新 */
    private Toast mToast;

    public void showToast(String msg) {
        if (mToast == null) {
            mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG);
        }
        mToast.setText(msg);
        mToast.show();
    }

    public int dp2px(int dp) {
        float density = this.getResources().getDisplayMetrics().density;
        return (int) (dp * density + 0.5f);
    }

}

2.6 布局文件的最终完善

<com.xiaowu.draglayout.view.DragLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drag_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/bg">

    <!-- 侧拉菜单界面 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center_vertical"
        android:padding="15dp" >

        <ImageView
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:background="@drawable/head"/>

        <ListView
            android:id="@+id/lv_menu"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="10dp" />

    </LinearLayout>

    <!-- 主菜单界面 -->
    <com.xiaowu.draglayout.view.MyLinearLayout
        android:id="@+id/my_ll"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        android:background="#fff">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:background="#18B4ED" >

            <ImageView
                android:id="@+id/iv_header"
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="10dp"
                android:background="@drawable/head" />
        </RelativeLayout>

        <ListView
            android:id="@+id/lv_main"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1" />

    </com.xiaowu.draglayout.view.MyLinearLayout>

</com.xiaowu.draglayout.view.DragLayout>

2.7 Constant静态类可以自己定义,不过我还是善良的贴了出来

package com.xiaowu.draglayout;

public class Constant {

    /** 菜单列表数据 */
    public static final String[] MENUS = new String[] {
        "纸杯蛋糕[Cupcake]",
        "甜甜圈[Donut]",
        "闪电泡芙[Eclair]",
        "冻酸奶[Froyo]",
        "姜饼[Gingerbread]",
        "蜂巢[Honeycomb]",
        "冰淇淋三明治[Ice Cream Sandwich]",
        "果冻豆[Jelly Bean]",
        "奇巧[KitKat]",
        "棒棒糖[Lollipop]",
        "姜饼[Gingerbread]",
        "蜂巢[Honeycomb]",
        "冰淇淋三明治[Ice Cream Sandwich]",
        "果冻豆[Jelly Bean]",
        "奇巧[KitKat]",
        "棒棒糖[Lollipop]",
        "棉花糖[Marshmallow]"
    };
    
    /** 列表数据1 */
    public static final String[] LIST_DATAS = {
        "API1--1.0 [没有开发代号]",
        "API2--1.1 Petit Four",
        "API3--1.5 Cupcake",
        "API4--1.6 Donut",
        "API5--2.0 Eclair",
        "API6--2.0.1 Eclair",
        "API7--2.1 Eclair",
        "API8--2.2 - 2.2.3 Froyo",
        "API9--2.3 - 2.3.2 Gingerbread",
        "API10--2.3.3-2.3.7 Gingerbread",
        "API11--3.0 Honeycomb",
        "API12--3.1 Honeycomb",
        "API13--3.2 Honeycomb",
        "API14--4.0 - 4.0.2 Ice Cream Sandwich",
        "API15--4.0.3 - 4.0.4 Ice Cream Sandwich",
        "API16--4.1 Jelly Bean",
        "API17--4.2 Jelly Bean",
        "API18--4.3 Jelly Bean",
        "API19--4.4 KitKat",
        "API20--4.4W",
        "API21--5.0 Lollipop",
        "API22--5.1 Lollipop",
        "API23--6.0 Marshmallow"
    };
}

三、一些可以借鉴的东西

  *比如使用Toast的时候可以采用单例模式,使得Toast可以随时改变,而不会产生停顿延迟的问题(顶部效果图)

/** toast使用单例模式,可以随状态刷新 */
    private Toast mToast;

    public void showToast(String msg) {
        if (mToast == null) {
            mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG);
        }
        mToast.setText(msg);
        mToast.show();
    }

四、提供给博友我的源代码

    **下载链接:

http://pan.baidu.com/s/1o8k4cZo  密码:m0fl


作者:吴小