Android 自定义侧滑菜单

59 篇文章 0 订阅
15 篇文章 0 订阅

前言

其实对于侧滑菜单,在博主刚开始学android接触到的时候,博主是非常感兴趣的,也非常想知道它是如何实现的,在技术的不断上升之后,我也可以自己封装侧滑菜单了.虽然网上有太多的成品等着我去用,但是博主本着学习和分享的态度还是决定写下这篇博客,也不介意再重复的造一次轮子,但是我相信,每一次重复的造轮子,都是对我技术最好的检验!好了下面就带大家来封装这个神奇的侧滑菜单吧

阅读的你需要了解以下的类

用于计算平滑的滑动的时候的运动轨迹的类:Scroller

用于计算用户手指在屏幕上产生的速度的类:VelocityTracker

实现的效果图


可以看到实现的效果还是挺棒的,第二种效果和第一种之间就仅仅存在一点区别,就是右边的视图会有一个缩放的效果

如果感兴趣的话,就跟我一起把这个轮子造出来吧!


分析

其实大家脑子里面应该能想到,菜单和主界面这是一个怎么样的摆放,但是博主还是给出一张自己画的分析图,菜单在左边为例:

图中黑色为手机屏幕

红色是菜单视图

蓝色是主界面视图

这是菜单没有滑动出来的时候


这是菜单滑动出来的时候


图画的不好,大家将就着看一下,其实就是菜单从左边慢慢的滑出的一个过程

所以其实视图的摆放还是挺简单的

1.菜单和主界面紧挨着

2.菜单在左边,主界面在右边,主界面的大小就是屏幕的大小(其实是侧滑菜单控件的大小,但是为了更好的讲解,这里策划菜单的大小和屏幕是一样的,所以这里就说主界面和屏幕一样大的,实际大小是你在布局文件中指定侧滑控件的大小哦)


自定义控件几步走

1.选择需要继承的类并且重写必要的构造方法--几乎通用

/**
 * Created by cxj on 2016/5/15.
 *
 * @author 小金子
 *         这个是一个自定义的层叠式的侧滑菜单
 *         请遵循一下使用原则:
 *         1.如果是两个孩子的情况
 *         第一个孩子是菜单,默认是左边
 *         第二个是主界面
 *         3.不是两个孩子都会报错
 */
public class ScaleSlideMenu extends ViewGroup {

    public ScaleSlideMenu(Context context) {
        this(context, null);
    }

    public ScaleSlideMenu(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScaleSlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    /**
     * 初始化
     *
     * @param context
     */
    private void init(Context context) {
        this.context = context;
        scroller = new Scroller(context);
        vt = VelocityTracker.obtain();
    }
}
构造函数之后用this,这样子只需要在三个参数的构造函数中调用初始化的方法


2.重写onMeasure方法和onLayout方法--几乎通用

onMeasure方法测量孩子和自身

onLayout方法排列孩子的位置

这里不对这两个方法做深入的讲解,所以不太懂的人请自行百度先了解了解


onMeasure方法的实现

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //检查孩子的个数
        checkChildCount();

        //宽度的计算模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        //推荐的宽度的值
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        //高度的计算模式
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //推荐的高度的值
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //菜单
        View menuView = getChildAt(0);
        //生成菜单的宽度的计算模式和大小,菜单的宽度大小受一个百分比限制
        int menuWidthSpec = MeasureSpec.makeMeasureSpec((int) (widthSize * menuPercent), MeasureSpec.AT_MOST);
        //生成菜单的高度的计算模式和大小
        int menuHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST);
        //让菜单的试图部分去测量自己
        menuView.measure(menuWidthSpec, menuHeightSpec);


        //主界面
        View mainView = getChildAt(1);
        //生成主界面的宽度的计算模式和大小
        int mainWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
        //生成主界面的高度的计算模式和大小
        int mainHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST);
        //让主界面的试图部分去测量自己
        mainView.measure(mainWidthSpec, mainHeightSpec);

        //侧滑控件的大小呢就直接采用父容器推荐的,所以
        //ViewGroup.LayoutParams.WRAP_CONTENT 和 ViewGroup.LayoutParams.MATCH_PARENT 的情况下大小是一致的,这里做一个说明
        //作用是保存自身的大小,没有这句话控件不显示,也就是看不到啦
        setMeasuredDimension(widthSize, heightSize);

        //保存自身的宽度和高度,其他地方有用
        mWidth = getWidth();
        mHeight = getHeight();
    }
注释写的很纤细了,这里不在全部陈述,只说明一点

这里菜单的宽度是由一个比例值算出来的,是整个策划菜单宽度的百分之几,这个变量可以从代码中知道是menuPercent,这里就给使用的人提供了方便,可以修改这个值,来实现自己想要的菜单大小

有关的变量和常量这里贴出

    /**
     * 最小的菜单宽度占用百分比
     */
    public static final float MIN_MENUPERCENT = 0.5f;

    /**
     * 最大的菜单宽度的占用百分比
     */
    public static final float MAX_MENUPERCENT = 0.8f;

    /**
     * 菜单宽度占屏幕的比例,默认是最小的
     */
    private float menuPercent = MAX_MENUPERCENT;

onMeasure方法关联的方法的代码

    /**
     * 检查孩子个数
     */
    private void checkChildCount() {
        int childCount = getChildCount();
        if (childCount != 2) {
            throw new RuntimeException("the childCount must be 2");
        }
    }
这个方法来检查孩子的个数,如果不是两个,直接让程序挂掉就行了

那么整个测量的方法就介绍完了,其实挺简单的,而且很多都是套路,你懂得,就是代码几乎都一样嘛


onLayout方法的实现

此方法就是计算出每一个孩子的位置信息,然后通过View.layout(l,t,r,b)方法来保存或者设置每一个孩子的位置信息

所以在保存位置信息之前是不是需要计算每一个孩子的位置信息先呐?

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        //计算所有孩子的位置信息
        computePosition();

        //获取孩子的个数
        int childCount = getChildCount();
        int size = rectEntities.size();

        for (int i = 0; i < size && i < childCount; i++) {

            View v = getChildAt(i);
            RectEntity rectEntity = rectEntities.get(i);

            v.layout(rectEntity.leftX, rectEntity.leftY, rectEntity.rightX, rectEntity.rightY);
        }

    }

    /**
     * 存储孩子的位置信息,由方法{@link ScaleSlideMenu#computePosition()}计算而来
     */
    private List<RectEntity> rectEntities = new ArrayList<RectEntity>();

    /**
     * 计算所有孩子的位置信息
     *
     * @return
     */
    private void computePosition() {
        rectEntities.clear();
        //获取孩子的个数
        int childCount = getChildCount();

        if (childCount == 2) { //如果是两个孩子的情况
            //第一个孩子是一个菜单
            View menuView = getChildAt(0);

            //闯进第一个孩子的位置参数
            RectEntity menuRect = new RectEntity();
            if (menuGravity == MENU_GRAVITY_LEFT) { //如果菜单是在左边的
                menuRect.rightX = 0;
                menuRect.leftX = menuRect.rightX - menuView.getMeasuredWidth();
            } else {
                menuRect.leftX = mWidth;
                menuRect.rightX = menuRect.leftX + menuView.getMeasuredWidth();
            }

            menuRect.leftY = 0;
            menuRect.rightY = menuRect.leftY + menuView.getMeasuredHeight();

            //第二个孩子是一个主界面
            View mainView = getChildAt(1);

            //闯进第一个孩子的位置参数
            RectEntity mainRect = new RectEntity();
            mainRect.leftX = 0;
            mainRect.rightX = mainRect.leftX + mainView.getMeasuredWidth();
            mainRect.leftY = 0;
            mainRect.rightY = mainRect.leftY + mainView.getMeasuredHeight();

            //添加位置信息到集合
            rectEntities.add(menuRect);
            rectEntities.add(mainRect);

        } else { //如果是三个孩子的情况

        }

    }

此方法涉及到一个对象RectEntity,其实就是Java的面向对象思想,把一个孩子的位置信息封装到一个类中

这里有四个变量分别是矩形左上角的横纵坐标和右下角的横纵坐标,可能有人有疑问,这两个点就能确定一个矩形了?

显然不能,但是这里的矩形隐含了一个条件,就是矩形是水品放置的,所以左上角的坐标和右下角的坐标已经足够了

/**
 * 一个实体类,描述一个矩形的左上角的点坐标和右下角的点的坐标
 * 
 * @author cxj QQ:347837667
 * @date 2015年12月22日
 *
 */
public class RectEntity {

	// 左上角横坐标
	public int leftX;

	// 左上角纵坐标
	public int leftY;

	// 右下角横坐标
	public int rightX;

	// 右下角纵坐标
	public int rightY;

}

这些代码就是计算出了菜单在左边或者在右边的时候的位置信息,还有主界面的位置信息,保存到了一个集合中,为什么要保存到集合中,这是因为位置信息在其他地方还可能有用,如果你直接把这些代码写到onLayout方法中,计算出来就直接调用View.layout方法,那么其他地方想要用到位置信息就十分的麻烦,所以这里把代码分离了,也让整体带么显的更清晰,也降低了耦合性

到这里为止,其实就已经可以看到主界面的试图了,不过还不能有任何的效果和滑动,这个下面马上带你实现

这里先给出我随便弄得布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:background="@drawable/bg"
    tools:context="com.example.cxj.fragmentscalemenu.MainActivity">

    <!--侧滑菜单-->
    <xiaojinzi.view.scaleSlideMenu.ScaleSlideMenu
        android:id="@+id/sm"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!--第一个孩子,是左边的菜单-->
        <include layout="@layout/menu" />

        <!--主界面-->
        <include layout="@layout/main" />

    </xiaojinzi.view.scaleSlideMenu.ScaleSlideMenu>
</RelativeLayout>
这里可以看到策划菜单这个是填充父容器的,没有任何内边距和外边距,那么其实就是屏幕的大小啦
侧滑菜单中有两个孩子,一个是我们的菜单,另一个是主界面,给出xml的布局,大家一目十行的看一下啦


菜单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">

    <!--菜单用一个listview演示一下-->
    <ListView
        android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></ListView>

</LinearLayout>

主界面xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!--标题栏-->
    <LinearLayout
        android:id="@+id/titlebar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#0000FF"
        android:orientation="horizontal"
        android:padding="10dp">

        <ImageView
            android:id="@+id/menu"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/menu" />

    </LinearLayout>

    <!--一个图片-->
    <ImageView
        android:id="@+id/iv_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/bt"
        android:layout_below="@+id/titlebar"
        android:scaleType="fitXY"
        android:src="@mipmap/content" />

    <!--底部的按钮-->
    <Button
        android:id="@+id/bt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:onClick="clickView"
        android:text="点我测试" />

</RelativeLayout>

MainActivity的代码:

public class MainActivity extends Activity {

    /**
     * 侧滑控件
     */
    private ScaleSlideMenu sm = null;

    /**
     * 主界面左上角的小菜单图标
     */
    private ImageView iv_menu = null;

    /**
     * 显示菜单数据的
     */
    private ListView lv = null;

    /**
     * listview要用到的数据
     */
    private List<String> data = new ArrayList<String>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        //寻找控件
        sm = (ScaleSlideMenu) findViewById(R.id.sm);
        iv_menu = (ImageView) findViewById(R.id.menu);
        lv = (ListView) findViewById(R.id.lv);
        

        //准备菜单的数据
        for (int i = 0; i < 20; i++) {
            data.add("菜单条目" + i);
        }

        lv.setAdapter(new CommonAdapter<String>(this, data, android.R.layout.simple_list_item_1) {
            @Override
            public void convert(CommonViewHolder h, String item, int position) {
                h.setText(android.R.id.text1, item);
            }
        });


    }

    public void clickView(View view) {
        Toast.makeText(this, "点我按钮了", Toast.LENGTH_LONG).show();
    }

}

这些大家简单的看一下就好了,都是十分简单的布局和一些初始化数据和试图的代码,这里适配器用了通用的适配器,不懂的或者不会用的请自行百度,我认为它是非常好用的哦


代码到这里,你运行以后应该就是下面这样子了


不具备任何的效果和滑动


为侧滑控件添加滑动效果--根据自己控件的特点编写

要有滑动效果,肯定是手指滑动吧?所以肯定是重写onTouchEvent方法吧?所以,go

    private int currentX;
    private int currentY;
    private int finalX;
    private int finalY;

    @Override
    public boolean onTouchEvent(MotionEvent e) {

        //获取事件的类型(动作)
        int action = e.getAction();

        switch (action & MotionEvent.ACTION_MASK) {

            case MotionEvent.ACTION_DOWN: //按下
                
                //保存按下的时候的坐标
                currentX = (int) e.getX();
                currentY = (int) e.getY();

                break;

            case MotionEvent.ACTION_MOVE: //移动

                //保存移动之后的新的坐标点
                finalX = (int) e.getX();
                finalY = (int) e.getY();

                //如果新旧坐标不一致
                if (finalX != currentX || finalY != currentY) {
                    
                    //计算水平距离和垂直距离
                    int dx = finalX - currentX;
                    int dy = finalY - currentY;

                    //让整个试图跟着手指移动起来
                    scrollBy(-dx, 0);

                    //移动之后旧的坐标进行更新
                    currentX = (int) e.getX();
                    currentY = (int) e.getY();
                }

                break;
            case MotionEvent.ACTION_UP://抬起

                
                break;
        }

        return true;
    }

代码都是看得懂的,注释写的很详细了哦,所以不在解释,最后返回一个true,表示事件传递到我这里我就要吃掉啦!

加上这段代码之后,你的策划菜单就可以滚动啦!目前的效果


可以看到我们的试图是可以滚动了

然后先实现平滑的打开菜单和关闭菜单,这里提供给用户两个方法,分别用来打开和关闭菜单

    /**
     * 打开菜单
     */
    public void openMenu() {
        if (menuGravity == MENU_GRAVITY_LEFT) {
            //拿到菜单的位置参数
            RectEntity menuRect = rectEntities.get(0);
            smoothTo(menuRect.leftX);
        } else {
            //拿到菜单的位置参数
            RectEntity menuRect = rectEntities.get(0);
            smoothTo(menuRect.rightX - menuRect.leftX);
        }

        preIsMenuOpen = isMenuOpen;
        isMenuOpen = true;
    }

    /**
     * 关闭菜单
     */
    public void closeMenu() {
        smoothTo(0);
        preIsMenuOpen = isMenuOpen;
        isMenuOpen = false;
    }

其实就是计算出打开菜单或者关闭菜单的时候要滑动的目标点的横坐标,然后调用smoothTo方法

    /**
     * 平滑的移动到指定位置
     */
    private void smoothTo(int finalX) {
        isScrolling = true;
        scroller.startScroll(getScrollX(), 0, finalX - getScrollX(), 0, defalutDuring);
        removeRepeatData();//消除重复数据
        scrollTo(scroller.getCurrX(), 0);
    }
在这里可以看到Scroller类被使用到了,它帮我们产生从起始点到重点过程中的轨迹点

但是数据并不是一次性全部产生的,而是每次调用scroller.computeScrollOffset();方法后才会产生下一个新的数据

比如(1,1)到(10,10),产生的一些列数据大概如下:

(1,1),(2,2),(3,3),(4,4)......(10,10)

当然了,它产生的数据可比我举例的多得多,甚至很多都是重复的数据,也就是所谓的比较密集吧,所以这里为了消除重复的那些数据,编写了一个removeRepeatData()的方法来消除重复的数据,让它新旧数据之间相差至少为1

    /**
     * 用于消除滑动的时候出现的重复数据
     * 因为平滑的滑动的时候产生的数据很多都是重复的
     * 都是所以这里如果遇到事重复的就拿下一个,直到不重复为止
     * 1.当前的值{@link View#getScrollX()}不等于{@link Scroller#getCurrX()}
     * 2.
     */
    private void removeRepeatData() {
        //获取当前的滚动的值
        int scrollX = getScrollX();
        //如果当前的值不是最后一个值,并且当前的值等于scroller中的当前值,那么获取下一个值
        while (scrollX != scroller.getFinalX() && scrollX == scroller.getCurrX()) {
            scroller.computeScrollOffset();
        }
    }


scrollTo(int x, int y)方法是View中的方法,可以让试图滚动到某一点,而我们要使用试图平滑的滚动,你肯定不能直接滚动到目标点,你肯定需要让Scroller类产生轨迹,然后反复的调用scrollTo(int x, int y)方法滚动到每一个点,从而实现效果上的平滑滚动
所以方法smoothTo中的scrollTo(scroller.getCurrX(), 0);语句就是让试图滚动到了第一个点
在View中当滚动完成后会调用computeScroll()方法,所以我们就可以在这个方法中让视图继续滚动到下一个点
    @Override
    public void computeScroll() { //当View完成滚动的时候调用
        //如果轨迹中还有没有滚完的点
        if (scroller.computeScrollOffset()) {
            removeRepeatData();//消除重复数据
            scrollTo(scroller.getCurrX(), 0);
        }
    }
上述实现平滑的滚动的方法是每一个能平滑滚动的控件的一个基本的实现方法,所以阅读的你,需要掌握!
我们给我们的小菜单按钮添加一个监听


然后看现在的效果图



到这里为止实现了平滑的打开菜单,关闭菜单同理,不再详细解释

但是拖动的时候只是拖动到哪里就是哪里,不会自动判断当前的位置是应该打开菜单还是关闭菜单

所以下一步就是在手指抬起的时候判断当前的位置是应该打开菜单还是关闭菜单

判断之后让试图平滑的滑动到位置

触摸事件处理详解--根据控件功能定制

但是还有其他细节的处理,所以这里就直接给出了全部的触摸的事件的处理代码,还请见谅,博主会做一个详细的解释

    private int currentX;
    private int currentY;
    private int finalX;
    private int finalY;

    /**
     * 是否移动了
     */
    private boolean isMove;

    /**
     * 是否正在滚动
     */
    private boolean isScrolling;

    /**
     * 事件在经过孩子的事件分发之后才到这里的
     *
     * @param e
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent e) {

        //获取事件的类型(动作)
        int action = e.getAction();

        //如果不需要侧滑出菜单,并且菜单是关闭状态
        if (isScrolling) {
            return false;
        }

        vt.addMovement(e);

        switch (action & MotionEvent.ACTION_MASK) {

            case MotionEvent.ACTION_DOWN: //按下

                //保存按下的时候的坐标
                currentX = (int) e.getX();
                currentY = (int) e.getY();

                break;

            case MotionEvent.ACTION_MOVE: //移动

                //保存移动之后的新的坐标点
                finalX = (int) e.getX();
                finalY = (int) e.getY();

                //如果新旧坐标不一致
                if (finalX != currentX || finalY != currentY) {

                    //计算水平距离和垂直距离
                    int dx = finalX - currentX;
                    int dy = finalY - currentY;

                    //让整个试图跟着手指移动起来
                    scrollBy(-dx, 0);

                    scaleMainView();

                    //移动之后旧的坐标进行更新
                    currentX = (int) e.getX();
                    currentY = (int) e.getY();
                    isMove = true;
                }

                break;
            case MotionEvent.ACTION_UP://抬起

                if (isMove) {
                    //计算速度
                    vt.computeCurrentVelocity(1000, Integer.MAX_VALUE);
                    //水平方向的速度
                    float xVelocity = vt.getXVelocity();

                    //如果速度的绝对值大于200,我们就认为试图有一个抛出的感觉

                    //如果速度达到了
                    if (xVelocity > 200) {
                        if (menuGravity == MENU_GRAVITY_LEFT) {
                            openMenu();
                        } else {
                            closeMenu();
                        }
                    } else if (xVelocity < -200) {
                        if (menuGravity == MENU_GRAVITY_LEFT) {
                            closeMenu();
                        } else {
                            openMenu();
                        }
                    } else {
                        judgeShouldSmoothToLeftOrRight();
                    }
                } else {
                    closeMenu();
                }
                break;
        }

        return true;
    }

关联的方法,用于判断当前的点是需要关闭菜单还是打开菜单

   /**
     * 判断当前的位置应该是滑出菜单还是关闭菜单
     */
    private void judgeShouldSmoothToLeftOrRight() {
        //菜单的位置参数对象
        RectEntity menuRect = rectEntities.get(0);
        if (menuGravity == MENU_GRAVITY_LEFT) { //如果菜单在左边
            //菜单左边的横左边
            int menuLeftX = menuRect.leftX;
            //获取到当前的位置
            int scrollX = getScrollX();
            if (menuLeftX / 2 > scrollX) {
                openMenu();
            } else {
                closeMenu();
            }
        } else {
            //菜单的宽度
            int menuWidth = menuRect.rightX - menuRect.leftX;
            //获取到当前的位置
            int scrollX = getScrollX();
            if (menuWidth / 2 > scrollX) {
                closeMenu();
            } else {
                openMenu();
            }
        }

    }

在onTouchEvent从头开始看,有一句vt.addMovement(e);

还记得开头前言的下面我有说过这个vt这个类的作用,是一个可以计算出用户在触摸的时候产生的速度的一个类,用法很简单,就是调用vt.addMovement(e);,在抬起的时候你就可以获取速度啦


在移动的动作事件里面有这么一句话:scaleMainView();这个最后解释


最后看抬起事件中的代码,通过判断isMove变量得知,手指在屏幕中是否滑动了,如果滑动了则计算出因滑动产生的速度,如果速度如注释所说,就有一个抛出的感觉,那么根据速度的方向和菜单的位置来选择应该是打开还是关闭菜单

抛出的这段的效果来一张



可以看到效果棒棒哒~~~~


最后来揭露一下为什么这里的缩放效果我没介绍,但是上述的效果中都已经实现了,这是因为真的就是一行代码

介绍一个方法:

    /**
     * 缩放主界面和菜单
     */
    private void scaleMainView() {
        if (slideMode == SCALE_SLIDE_MODE) { //如果模式是缩放的模式
            float percent = ((Number) Math.abs(getScrollX())).floatValue() / ((Number) Math.abs(rectEntities.get(0).leftX)).floatValue();
            //缩放主界面
            getChildAt(1).setScaleX(1f - 0.2f * (percent));
            getChildAt(1).setScaleY(1f - 0.2f * (percent));
            //缩放菜单
            getChildAt(0).setScaleX(0.6f + 0.4f * (percent));
            getChildAt(0).setScaleY(0.6f + 0.4f * (percent));
        }
    }


这个方法的注释写出了这个方法的功能,就是缩放主界面,而方法中是根据什么进行缩放的呢?

就是根据当前的视图滚动的偏移量和菜单的宽度的计算出百分比

然后使用这个百分比设置主界面View的缩放比例,这里利用数学上的知识做了一个限制,可以看到

percent的值区间是[0-1],所以整个1-0.4*percent的区域就是[0.6-1],所以不会出现缩放太夸张的现象!

所以这个方法在视图滚动的时候调用即可实现缩放的效果!

第一个需要调用这个方法地方就是平滑滚动的时候

    @Override
    public void computeScroll() { //当View完成滚动的时候调用
        //如果轨迹中还有没有滚完的点
        if (scroller.computeScrollOffset()) {
            removeRepeatData();//消除重复数据
            scrollTo(scroller.getCurrX(), 0);
            scaleMainView();
        }
    }

第二个需要调用的地方就是我们的手指去滑动的时候

            case MotionEvent.ACTION_MOVE: //移动

                //保存移动之后的新的坐标点
                finalX = (int) e.getX();
                finalY = (int) e.getY();

                //如果新旧坐标不一致
                if (finalX != currentX || finalY != currentY) {

                    //计算水平距离和垂直距离
                    int dx = finalX - currentX;
                    int dy = finalY - currentY;

                    //让整个试图跟着手指移动起来
                    scrollBy(-dx, 0);

                    scaleMainView();

                    //移动之后旧的坐标进行更新
                    currentX = (int) e.getX();
                    currentY = (int) e.getY();
                    isMove = true;
                }

                break;

好了,到这里就基本上完工了,最后的最后需要处理的一点事情就是事件冲突的处理,其实也就是在某些时候事件是需要被侧滑菜单本身拦截的,而不应该传递给孩子,所以这也是下面需要讲到的事件的拦截

1.菜单打开的时候

菜单部分的视图的事件不拦截,但是主界面的事件要绝对的拦截!这个效果应该你们都可以想象出来,打开菜单后如果点击主界面,会收回菜单,效果如下:


2.菜单没有打开的时候

先看效果


如果按下的时候距离边缘的横向距离是符合滑动出菜单的,但是可以看到并不是直接就从ACTION_DOWN开始拦截的,因为此时并不能知道用户是要滑动出菜单还是滑动主界面的ListView

所以当菜单是关闭的时候欧,拦截还是不拦截是根据两点:

1.手指按下的时候的距离边缘的距离符合滑动出菜单的限制

2.手指滑动的时候水平方向的dx滑动多于竖直方向的dy

所以onInterceptTouchEvent(MotionEvent ev)方法你也应该能看得懂了,注释写的也挺详细的,其实就是做了以下几点事情:

1.在菜单打开的时候拦截主界面的事件

2.在菜单关闭的时候,如果手指按下的时候的坐标符合滑出菜单的条件并且水平滑动的距离多于竖直滑动的距离,那么也拦截主界面事件

    /**
     * 是否拦截孩子的事件
     */
    private boolean isInterceptTouchEvent = false;

    /**
     * 是否已经判断过事件属于谁了
     */
    private boolean isAdjustEvent = false;

    /**
     * 手指接触屏幕的时候的坐标
     */
    private Point downPoint = new Point();

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        //获取事件的类型
        int action = ev.getAction();

        //筛选事件
        switch (action & MotionEvent.ACTION_MASK) {

            case MotionEvent.ACTION_DOWN: //按下的情况下

                isInterceptTouchEvent = false;
                isAdjustEvent = false;
                isMove = false;
                vt.clear();

                //不管拦截不拦截,先保存按下时候的坐标
                downPoint.x = (int) ev.getX();
                downPoint.y = (int) ev.getY();

                //拦截菜单打开的情况下主界面的事件
                if (isMenuOpen) {

                    //菜单的位置参数对象
                    RectEntity menuRect = rectEntities.get(0);

                    //如果判断出来按下的时候坐标是菜单的区域,则直接拦截,这个事件不传递给孩子
                    if (menuGravity == MENU_GRAVITY_LEFT) {
                        if (downPoint.x > Math.abs(menuRect.leftX)) {
                            isAdjustEvent = true;
                            isInterceptTouchEvent = true;
                        }
                    } else {
                        if (downPoint.x < (mWidth - (menuRect.rightX - menuRect.leftX))) {
                            isAdjustEvent = true;
                            isInterceptTouchEvent = true;
                        }
                    }
                }else{

                }
                break;
            case MotionEvent.ACTION_MOVE: // 移动

                if (isAdjustEvent) { //如果已经判断过事件了
                    return isInterceptTouchEvent;
                }

                //记录下当前移动后的点
                finalX = (int) ev.getX();
                finalY = (int) ev.getY();

                //菜单没有打开
                if (!isMenuOpen) {
                    //计算坐标之间的差值
                    int dx = Math.abs(finalX - downPoint.x);
                    int dy = Math.abs(finalY - downPoint.y);

                    if (dx > dy) { //说明水平方向的滑动多余竖直方向的,需要拦截
                        if (menuGravity == MENU_GRAVITY_LEFT) { //如果菜单在左边
                            if (downPoint.x < mWidth * slidePercent) {
                                isAdjustEvent = true;
                                isInterceptTouchEvent = true;
                                currentX = downPoint.x;
                                currentY = downPoint.y;
                            }
                        } else { //菜单在右边
                            if (downPoint.x > (mWidth - mWidth * slidePercent)) {
                                isAdjustEvent = true;
                                isInterceptTouchEvent = true;
                                currentX = downPoint.x;
                                currentY = downPoint.y;
                            }
                        }
                    }
                }
                break;
        }

        return isInterceptTouchEvent;
    }



好了,全部的代码完工了,这篇博客已经尽我所能了,自己觉得讲的是很详细了,就是希望阅读者能在自定义控件上面多学一点

如果你喜欢本博客或者有任何问题,请留言!

源码下载

https://github.com/xiaojinzi123/xiaojinzi-openSource-view/tree/master/xiaojinzi/view/scaleSlideMenu

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值