QQ测拉效果实现(三)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_32059827/article/details/78102343

转载本文请注明出处,尊重原创:

如果想第一时间收到文章更新,可以微信扫描二维码关注我的公众号,或者微信直接搜索“Android小菜”进行关注,所有的文章会比CSDN更快一步:


前两篇通过HorizontalScrollView + LinearLayout + scrollTo + 属性动画的知识实现了一个仿QQ5.0效果的控件。本篇纯手工实现类似的测拉效果。

先看最终要实现什么样的效果:



基本要点:自定义ViewGroup类型控件,需要两块布局分别是左侧菜单left和右侧主内容区域content,然后分别测量两边的大小以及布局,重写onTouchEvent方法进行事件的交互,使用Scroll进行平滑移动。

先把架构搭起来:

left布局:

扫描二维码关注公众号,回复: 3758759 查看本文章

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="200dp"
            android:background="@drawable/menu_bg"
            android:layout_height="match_parent">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!--每一个item-->
        <TextView
            style="@style/tab_item_style"
            android:drawableLeft="@drawable/tab_focus"
            android:text="ITEM2"
            />
        .....省略
    </LinearLayout>

</ScrollView>
left布局文件很简单,外层嵌套ScrollView,内部放置TextView作为测拉菜单的item项。然后每个item都一样,所以抽取了样式:

<style name="tab_item_style">
    <item name="android:background">@drawable/tab_item_selector</item>
    <item name="android:gravity">center_vertical</item>
    <item name="android:drawablePadding">20dp</item>
    <item name="android:paddingBottom">15dp</item>
    <item name="android:paddingTop">15dp</item>
    <item name="android:paddingLeft">20dp</item>
    <item name="android:textSize">24sp</item>
    <item name="android:textColor">@android:color/white</item>
    <item name="android:layout_width">match_parent</item>
    <item name="android:layout_height">wrap_content</item>
</style>

上面代码展示效果如下:


然后是主页面内容

<?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:layout_height="match_parent">

    <!--顶部栏-->
    <LinearLayout
        android:orientation="horizontal"
        android:background="@drawable/top_bar_bg"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">


        <ImageView
            android:id="@+id/iv_menu"
            android:src="@drawable/main_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <ImageView
            android:src="@drawable/top_bar_divider"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:paddingLeft="65dp"
            android:layout_gravity="center"
            android:textSize="25sp"
            android:textColor="@android:color/white"
            android:text="主页面"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </LinearLayout>

    <!--内容区域-->
    <TextView
        android:gravity="center"
        android:textSize="50dp"
        android:textColor="@android:color/black"
        android:text="ITEM1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>
它也很简答,一个标题栏,一个TextView,如下:


然后自定义SlidingMenu继承自ViewGroup:

public class SlidingMenu extends ViewGroup {

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

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

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

    }

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

    }
}
然后在activity_main布局里面加入我们的控件:

<?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:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.itydl.slidingmenu.MainActivity">

    <com.itydl.slidingmenu.SlidingMenu
        android:background="@color/colorAccent"
        android:id="@+id/slidingmenu"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

        <!--左侧菜单-->
        <include layout="@layout/left"/>

        <!--右侧内容-->
        <include layout="@layout/content"/>

    </com.itydl.slidingmenu.SlidingMenu>

</RelativeLayout>
此时运行程序后,是不会显示任何东西。为了能更好的理解,我在 SlidingMenu布局里面加了一个背景,此时运行,肯定能让SlidingMenu控件展示出来,但是他里面的孩子控件不会正常展示:下图为手机运行后效果:


接下来的任务就是开发自定义控件了:

重写onMeasure();onLayout();方法:

/**
     * XML解析完成调用此方法
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mLeftMenu = getChildAt(0);
        mMainContent = getChildAt(1);

        mLeftWidth = mLeftMenu.getLayoutParams().width;
//        Log.e(TAG,"mLeftWidth : --->"+mLeftWidth);
    }

    /**
     * 测量孩子控件的大小
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //测量左侧
        int  leftWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mLeftWidth,MeasureSpec.EXACTLY);
        mLeftMenu.measure(leftWidthMeasureSpec,heightMeasureSpec);

        //测量右侧
        mMainContent.measure(widthMeasureSpec,heightMeasureSpec);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * 布局孩子控件
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
//        Log.e(TAG,"mLeftMenu.getMeasuredWidth() : --->"+mLeftMenu.getMeasuredWidth());
//        Log.e(TAG,"mMainContent.getMeasuredWidth() : --->"+mMainContent.getMeasuredWidth());
        mLeftMenu.layout(-mLeftMenu.getMeasuredWidth(),0,0,mLeftMenu.getMeasuredHeight());
        Log.e(TAG,"this.width : --->"+this.getMeasuredWidth());
        mMainContent.layout(0,0,r,b);
    }
以上重写了三个方法,而且特别的简单,在 onFinishInflate() 方法在xml解析完成后调用,在里面可以拿到控件的实例。然后onMeasure();里面对控件做了测量,因为ViewGroup类型的自定义View需要对孩子控件测量大小,最终才会确认自己的大小。在这里,由于左侧菜单的宽度是自定义的一个宽度(比如上面写了240dp),而高度跟自定义View的高度是一样的,所以可以沿用我们自定义View的高度;然后右侧主内容就完全跟咱们的自定义View的宽高是一样的了,所以宽高都可以沿用自定义View的宽高。然后是 onLayout();方法,在这里面对控件进行了布局,这三个方法执行完毕后可以在手机上展示,上面可以用下面的图来表示出来:


但是这里跟上一篇的有点不一样,比如getScrollX(),上一篇的值如下:


它是一个正数,是因为上篇的自定义View继承的是HorizontalScrollView,它的计算是从整个绿色区域的左上角开始的。意思是说,往右滑动,getScrollX()的值变小,往左滑动,getScrollX()的值变大。如上图所示

而本篇文章的布局是直接继承自ViewGroup,而且布局中


这里的SlidingMenu跟屏幕一样,所以计算应该是从红色区域左上角计算的,比如,这里的getScrollX():



然后移动一点:


再看一张:


相信上面几张图,能明白这里跟上一篇的区别。

然后往下,重写onTouchEvent()进行交互事件:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mDownX = event.getX();
                    break;
                case MotionEvent.ACTION_MOVE:

                    float mMoveX = event.getX();
                    int diffX = (int) (mDownX - mMoveX + 0.5f);//四舍五入

                    Log.e(TAG,"diffX : --->"+diffX);
                    int scrolledX = getScrollX() + diffX;
                    Log.e(TAG,"getScrollX() : --->"+getScrollX());
                    if(scrolledX < -mLeftWidth){
                        scrollTo(-mLeftWidth,0);
                    }else if(scrolledX > 0){
                        scrollTo(0,0);
                    }else{
                        scrollBy(diffX,0);
                    }

                    mDownX = mMoveX;//保存移动后上一次的值
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e(TAG,"ACTION_UP : --->");
                    break;

                default:
                    break;
         }
        return true;
    }
重写 onTouchEvent()方法,返回值写为true,为了保证down事件以后,move和up事件都能顺利得到(down一旦返回值为了false,后序的move、up事件都不再调用)。上面的获取坐标的方式相信不用再去再做解释,主要在于scrollBy(x,y)和scrollTo(x,y)相关。主要是对边界的处理,不让一直滑动而出现滑动出界面的难看局面。比如scrolledX < -mLeftWidth 图形如下:这时候应该要让控件完全展示左侧菜单就截止。scrollTo(-mLeftWidth,0);


然后同理scrolledX > 0 。此时也属于越界,那么直接调用scrollTo(0,0);让控件回到(0,0)点。


int scrolledX = getScrollX() + diffX; 是为了解决边界出现“晃动”现象。


出现这种现象的原因是:如图


比如此时getScrollX为-mLeftWidth,此时继续move事件,往右滑动,此时的代码走红笔标注位置:


显然又往右滑动了一些距离,然后继续滑动,发现此时scrolledX<-mLeftWidth成立了,则会调用scrollTo(-mLeftWidth,0);

因此要时刻记录下最新的滑动了的位置,int scrolledX = getScrollX() + diffX;  比如当getScrollX() 为-mLeftWidth但是移动的距离也加上了,则若是往右滑动,这个值肯定满足scrolledX<-mLeftWidth这样就不会出现“晃动”效果了。


然后再往下,解决UP事件,UP的时候要让控件判断位置,过度到某个状态:


此时运行:


完成了基本的测拉效果,往下就是平滑过度一些。当然是使用Scroller

修改UP事件:


然后,写一个方法:switchMenu():以及其他方法:

    /**
     * 切换左侧菜单的展示与隐藏
     */
    private void switchMenu() {
        int startX = getScrollX();//当前x
        int dx = 0;
        if(CURRENT_STATE == CURRENT_CLOSE){
            //切换到关闭态
            dx = 0 - startX;
        }else if(CURRENT_STATE == CURRENT_OPEN){
            dx = -mLeftWidth - startX;
        }

        /**
         * 第三个值:目标 - startX;
         */
        scroller.startScroll(startX, 0, dx, 0, Math.abs(dx) * 5);//时间不确定,根据移动个数来算。Math.abs(dx)对dx取绝对值
        invalidate();// invalidate -> drawChild -> child.draw -> computeScroll
    }

    @Override
    public void computeScroll() {
        // 当scroller模拟完毕数据时,不再调用invalidate,
        if(scroller.computeScrollOffset()) {
            int currX = scroller.getCurrX();
            scrollTo(currX, 0);
            //这能调用一次,因此要循环调用invalidate();
            invalidate();//递归调用
        }
    }

    /**
     * 调用方来控制左侧菜单的打开与关闭
     */
    public void toogle(){
        if(CURRENT_STATE == CURRENT_OPEN){
            //关闭
            close();
        }else{
            //打开
            open();
        }
        switchMenu();
    }

    /**
     * 打开左侧菜单
     */
    private void open(){
        CURRENT_STATE = CURRENT_OPEN;
    }

    /**
     * 关闭左侧菜单
     */
    private void close(){
        CURRENT_STATE = CURRENT_CLOSE;
    }
可以看到是通过两个变量来表示当前的状态是打开还是隐藏左侧菜单,

   /**
     * 当前侧滑菜单处于打开状态
     */
    private static final int CURRENT_OPEN = 1;

    /**
     * 当前侧滑菜单处于打关闭状态
     */
    private static final int CURRENT_CLOSE = 2;

    /**
     * 当前策划菜单的状态,默认处于关闭状态
     */
    private static int CURRENT_STATE = CURRENT_CLOSE;
然后统一调用 switchMenu()方法,在这个方法里面使用创建的Scroller对象,这个对象会帮助我们做平滑移动(它内部是把距离不断的切分,然后再去调用scrollTo完成的)然后调用scroller.startScroll(startX, 0, dx, 0, Math.abs(dx) * 5);

这个方法有五个参数,分别是:1、移动的x起点位置;2、移动的y起点位置;3、距离差dx(等于目标值-起始值);4、代表y;5、时间差。

Math.abs(dx) * 5能让时间均衡,移动均衡。

然后调用invalidate();方法,它的执行流程是: invalidate -> drawChild -> child.draw -> computeScroll,然后重写了computeScroll方法,在他里面能拿到切分整个距离的当前值坐标。 int currX = scroller.getCurrX();拿到当前的x值,然后不断的调用scrollTo(currX, 0);进行移动到该值.打印一段log如下:
          -8,-5,-4,-3,-1,-1,0

  最后再运行程序:



此时所有的功能基本上已经实现了,但是,我们会发现,左侧菜单是ScrollView,竖向滑动可以实现,但是横向滑动,却没有任何效果。因此要解决滑动冲突:

其实很简单,只需要重写onInterceptTouchEvent,做好判断,如果是横滑,就让时间拦截掉,交给自己处理:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mDownX = ev.getX();
                    break;
                case MotionEvent.ACTION_MOVE:
                    float moveX = ev.getX();
                    int dx = (int) Math.abs(moveX - mDownX + 0.5);
                    if(dx > dip2px(mContext,8)){//如果是横滑,事件拦截掉,交给自己处理
                        return true;
                    }
                    break;

                default:
                    break;
         }
        return super.onInterceptTouchEvent(ev);
    }
最后再运行程序:



到此所有知识就讲解完毕了,喜欢可以点个赞。或者加个关注。

想要及时获取最新文章,请关注微信公众号:“Android小菜”。

猜你喜欢

转载自blog.csdn.net/qq_32059827/article/details/78102343