彷QQ侧滑菜单动画实现效果—ViewDragHelper

实现效果图

1、介绍自定义一款的彷QQ侧滑菜单,实现一系列动画展示效果。

     无论拿到那种UI的界面,第一步就我们要去分析这个界面的组合形成部分。
     实现效果:
     1、菜单布局UI和主页面UI的布局。
     2、设置NO_TITLE_BAR去除标题。
     3、菜单栏滑动和主页滑动,互相伴随效果,以及限制了界面的移动范围。
     4、在界面滑动的时候,菜单栏颜色由暗变亮的效果。
     5、涉及新的类**ViewDragHelper**

2、ViewDragHelper介绍及使用

      2013年开发者大会上提出;作用:专门用于ViewGroup中队子View进行拖拽;在19(Android4.4)以及以上的v4包中;
 本质是封装了对触摸事件的解析,包括触摸位置,触摸速度以及Scroller的封装,只需要我们在回调方法中指定是否移动,移动多少等等,
 但是需要注意的是:它只是一个触摸事件的解析类(如GestureDecetor),所以需要我们传递给它触摸事件,它才能工作;

3、其他不多讲,直接实现代码首先布局UI(有可能括号缺失)

这里写图片描述
1.主页面布局UI

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:background="#fff"
    android:layout_height="match_parent">

    <FrameLayout
        android:background="#18B4ED"
        android:layout_width="match_parent"
        android:layout_height="60dp">
        <ImageView
            android:layout_marginLeft="30dp"
            android:layout_width="50dp"
            android:id="@+id/iv_head"
            android:layout_gravity="center_vertical"
            android:src="@mipmap/head"
            android:layout_height="50dp" />
    </FrameLayout>
    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/main_listview"
        />
</LinearLayout>

2.侧拉菜单的页面布局UI(太长,暂不展示,需要布局留言)

这里写图片描述

3.include引入主页面,方便以后更改页面,itheima.com.slidemenuqq.SlideMenu是自定义的控件

<?xml version="1.0" encoding="utf-8"?>
<itheima.com.slidemenuqq.SlideMenu
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/slidemenu"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bg">
    <!--菜单界面-->
    <include layout="@layout/layout_menu"/>
    <!--主界面-->
    <include layout="@layout/layout_main"/>
</itheima.com.slidemenuqq.SlideMenu>

4、布局好UI之后,开始自定义控件

写一个SlideMenu类,继承自FrameLayout,因为如果继承自ViewGroup的话,需要我们自己来实现onMeasure方法,而该方法的实现一般比较麻烦且没有必要。
  所以选择继承系统的已有的控件FrameLayout,不用其他控件是因为FrameLayout最轻量级
public class SlideMenu extends FrameLayout {
    public SlideMenu(Context context) {
        this(context, null);
    }
    public SlideMenu(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public SlideMenu(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
}

5、创建ViewDragHelper对象

public class SlideMenu extends FrameLayout {
  ······
    public SlideMenu(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        /**创建对象**/
        viewDragHelper = ViewDragHelper.create(this, callback);

    }
}

6、由于ViewDragHelper只是触摸事件解析类,要想让ViewDragHelper工作,需要将触摸事件传递给它(固定格式)

/**
   * 让ViewDragHelper帮助我们判断是否应该拦截,Intercept  [,ɪntə'sept]拦截
   */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);
        return result;
    }
    /**
     * 
    public boolean onTouchEvent(MotionEvent event) {
        viewDragHelper.processTouchEvent(event);
        return true;
让ViewDragHelper帮我们处理触摸事件
     */
    @Override
    }

7、callback回调实现业务逻辑

ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {    }

8、在onFinishInflate方法中初始化子View的引用

/**
  * 该方法是当前view在布局文件中的xml结束标签读取完后执行,此时就知道,当前View有几个子view
  * 但是注意:此时还不能获取子view的宽和高,因为还没有测量
  */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        menu = getChildAt(0);//获取子控件菜单
        main = getChildAt(1);//获取子控件主页面
    }

9、中在onSizeChanged方法中初始化宽高,因为该方法在onMeasure之后执行,在这个方法里才能获取你想要的宽和高

/**
     * 该方法是onMeasure执行之后执行,因此可获取宽高,需要getMeasuredWidth()
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        maxLeft = (int) (getMeasuredWidth()*0.6f);
    }

10、(callback中)实现tryCaptureView,来判断要对哪个View进行触摸事件的捕获

/**
    * 尝试监视View的触摸事件
    * @param child     当前触摸的子View
    * @param pointerId   手指多点触摸时的触摸点的索引
    * @return  true表示监视 ,false就是忽略不管
    */
      @Override
     public boolean tryCaptureView(View child, int pointerId) {
         return child==main || child==menu;
     }

11、(callback中)实现getViewHorizontalDragRange,该方法必须要实现,返回的值自己来定义 ,只要大于0就行,否则在某些情况下不能水平滑动

  /**
         * 看起来是获取View水平的拖拽范围的,然而并不是这样,这是一个鸡肋的方法,目前它的作用是
         * 用来判断你是否想强制水平滑动的,如果想强制水平滑动,则返回大于0的任意值
         * @param child
         * @return
         */
        @Override
        public int getViewHorizontalDragRange(View child) {
            return 1;
        }

12、(callback中)重写clampViewPositionHorizontal方法,控制子View在水平方向上的移动

设置水平值之后

 /**
         * 修改View水平方向的位置移动,控制水平移动
         * @param child  当前触摸的子View
         * @param left   ViewDragHelper认为我想让View的left变成的值,它是这样计算好的:child.getLeft+dx
         * @param dx     本次手指移动的水平距离
         * @return       最终返回的值标示我们真正想让child的left变成的值
         */
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            if(child==main){
                left = clampLeft(left);
            }
            return left;
        }

13、(callback中)在onViewPositionChanged方法中实现一些伴随移动的效果,因为在该方法中可以获取view移动的距离

newLeft限制后效果图

1):/**
         * 当View位置改变的时候执行,伴随移动效果
         * @param changedView  当前位置改变的view
         * @param left         当前view改变后最新的left
         * @param top          当前view改变后最新的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);

            }
2): public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            Log.e("tag","left: "+left  + "  dx: "+dx);
            //触摸菜单滑动,让主页跟随移动
            if(changedView==menu){
                //手动让menu固定在原点位置
                menu.layout(0,0,menu.getMeasuredWidth(),menu.getBottom());
                //获取当前的left+移动距离
               int newLeft=main.getLeft()+dx;
                //对newLeft进行限制
                newLeft = clampLeft(newLeft);
                //使用layout进行布局
                main.layout(newLeft,0,newLeft+main.getMeasuredWidth(),main.getBottom());
            }
        }
3):/**
     * 抽取的方法,判断left的值
     */
    private int clampLeft(int left) {
        if(left>maxLeft){//标示滑动最大值为
            left=maxLeft;
        }else if(left<0){
            left=0;
        }
        return left;
    }

14、(callback中)在onViewReleased方法中处理手指抬起的缓慢移动

1):  /**
         * 手指抬起的时候执行缓慢效果
         * @param releasedChild  抬起的那个子View
         * @param xvel           x方向滑动的速度,单位是px/s
         * @param yvel           y方向滑动速度
         */
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
        }
2):public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            //如果当前的距离大于最大滑动距离1/2,就要打开,否则关闭菜单
            if(main.getLeft()>maxLeft/2){
                openMenu();
            }else{
                colseMenu();
            }
        }
    /**
     * 关闭菜单
     */
    private void colseMenu() {
        viewDragHelper.smoothSlideViewTo(main,0,0);
        ViewCompat.postInvalidateOnAnimation(SlideMenu.this);
    }
    /**
     * 打开菜单
     */
    private void openMenu() {
        //平滑效果
        viewDragHelper.smoothSlideViewTo(main,maxLeft,0);
        //用Invalidate也可以,建议从ViewCompat有更好的兼容性,
        // 参数:刷新整个view,调用computeScroll()
        ViewCompat.postInvalidateOnAnimation(SlideMenu.this);
    }
    //计算滑动
    @Override
    public void computeScroll() {
        super.computeScroll();
        //如果动画还没有结束,则继续刷新
        if(viewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(SlideMenu.this);
        }
    }

15、实现数据

这里写图片描述

public class MainActivity extends AppCompatActivity {
    private static final String[] NAMES = new String[]{"特别关心", "常用群聊", "我的好友",
            "小学同学", "中学同学", "高中同学", "大学同学", "亲朋好友", "公司同事"};
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ListView mListView = (ListView) findViewById(R.id.main_listview);
        //填充数据
        mListView.setAdapter(new MyAdapter());
    }
    private class MyAdapter extends BaseAdapter{
        @Override
        public int getCount() {
            return NAMES.length;
        }
        @Override
        public Object getItem(int i) {
            return null;
        }
        @Override
        public long getItemId(int i) {
            return 0;
        }
        @Override
        public View getView(int i, View view, ViewGroup viewGroup) {
            View item = View.inflate(MainActivity.this, R.layout.item_listview, null);
            TextView text = (TextView) item.findViewById(R.id.tv_ll);
            text.setText(NAMES[i]);
            return item;
        }
    }
}

16、(callback中)在onViewPositionChanged方法中实现动画效果

    //执行动画
            //1.获取main拖动的百分比:0——1f
            float fraction = main.getLeft()*1f / maxLeft;
            //2.根据百分比取执行一些列的伴随动画
            execAnim(fraction);

这里写图片描述

效果1:底层上涌
    private void execAnim(float fraction) {
        //fraction:0f - 1f
        //main执行缩放动画
        //scale: 1f - 0.8f
        //算法:startVal + (endVal-startVal)*fraction
        float scale = floatEval.evaluate(fraction,1f,0.8f);
        main.setScaleX(scale);
        main.setScaleY(scale);
        //menu执行缩放
        menu.setScaleX(floatEval.evaluate(fraction,0.3f,1f));
        menu.setScaleY(floatEval.evaluate(fraction,0.3f,1f)) 
 }

这里写图片描述

效果2:平移进入
private void execAnim(float fraction) {
        //fraction:0f - 1f
        //main执行缩放动画
        //scale: 1f - 0.8f
        //算法:startVal + (endVal-startVal)*fraction
        float scale = floatEval.evaluate(fraction,1f,0.8f);
        main.setScaleX(scale);
        main.setScaleY(scale);
        //menu执行缩放
        menu.setScaleX(floatEval.evaluate(fraction,0.3f,1f));
        menu.setScaleY(floatEval.evaluate(fraction,0.3f,1f));
        //menu执行平移效果
        menu.setTranslationX(floatEval.evaluate(fraction,-menu.getMeasuredWidth()/2,0));
    }

这里写图片描述

效果33D立体效果
private void execAnim(float fraction) {
        //fraction:0f - 1f
        //main执行缩放动画
        //scale: 1f - 0.8f
        //算法:startVal + (endVal-startVal)*fraction
        float scale = floatEval.evaluate(fraction,1f,0.8f);
        //立体3D效果
        main.setRotationY(floatEval.evaluate(fraction,0,90));
        menu.setRotationY(floatEval.evaluate(fraction,-90,0));
    }
给SlideMenu的背景图片添加阴影遮罩效果 
if(getBackground()!=null){
            int color = (int) argbEval.evaluate(fraction, Color.BLACK,Color.TRANSPARENT);
            //设置颜色过滤器,参数1:颜色、参数2:模式
            getBackground().setColorFilter(color, PorterDuff.Mode.SRC_OVER);
        }
(拓展)
颜色提供渐变器:功能渐变色彩
ArgbEvaluator argbEval = new ArgbEvaluator();
浮点计算器:内部算法: startVal + (endVal-startVal)*fraction

FloatEvaluator floatEval = new FloatEvaluator();

17、暴露接口,定义回调监听

OnSlideListener listener;
    public void setOnSlideListener(OnSlideListener listener){
        this.listener = listener;
    }
    //定义回调接口
    public interface  OnSlideListener{
        void onSliding(float fraction);
        void onOpen();
        void  onClose();
    }
在 onViewPositionChanged方法中 回调接口
 //3.回调接口的方法
            if(listener!=null){
                listener.onSliding(fraction);
                if(fraction==0f){
                    listener.onClose();
                }else if(fraction==1f){
                    listener.onOpen();
                }
            }

18、主页实现监听

 //添加滑动监听器
        slideMenu.setOnSlideListener(new SlideMenu.OnSlideListener() {
            @Override
            public void onSliding(float fraction) {
                ivHead.setRotation(720*fraction);
            }
            @Override
            public void onOpen() {
                Toast.makeText(MainActivity.this, "芝麻开门!", Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onClose() {
                Toast.makeText(MainActivity.this, "芝麻关门-----!", Toast.LENGTH_SHORT).show();
                ViewCompat.animate(ivHead)
                        .translationX(60)
//                     .setInterpolator(new CycleInterpolator(4))  // 循环执行,参数1:晃动的幅度
//                      .setInterpolator(new OvershootInterpolator(4))超过之后再回来,射击效果
                       .setInterpolator(new BounceInterpolator())//兵乓效果 Bounce [baʊns]弹跳
                        .setDuration(1000)
                        .start();
            }
        });
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值