自定义DrawerLayout抽屉布局

Posted zhdsky

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义DrawerLayout抽屉布局相关的知识,希望对你有一定的参考价值。

最近项目需求需要做一个抽屉滑动的效果,也就是从屏幕的右边滑进来页面,就像这样:

技术图片

很简单,系统自带的DrawerLayout就能完成这样基本的需求。但是我们的需求要稍微复杂一点,在关闭侧边栏之前啊,我们想去做一下数据保存。这时候怎么办呢?相信大家都会想,这东西肯定有滑动监听啊,去监听一下滑动事件不就好了吗,ok,那我们现在来看一下Drawerlayout的滑动监听,代码如下:

 

mDrawerLayout.addDrawerListener(new MyDrawerLayout.DrawerListener() {
            @Override
            public void onDrawerSlide(@NonNull View var1, float var2) {
                
            }

            @Override
            public void onDrawerOpened(@NonNull View var1) {

            }

            @Override
            public void onDrawerClosed(@NonNull View var1) {

            }

            @Override
            public void onDrawerStateChanged(int var1) {

            }
        });

 

我们看到系统给我们提供了四个滑动监听,最符合我们需求的看上去是这个方法,


@Override
public void onDrawerClosed(@NonNull View var1) {

}

 

 

那么它能实现在关闭动作开始之前回调到我们的页面吗?答案是,不能!这个方法的调用时机是在侧边栏滑动结束之后,也会是,等到侧边栏彻底的滑动到了屏幕的外面,滑动动作完全结束之后才会执行这个方法! emmmm,shitaaa

那么其他的方法好使吗?答案是,不好使,至于为什么不好使,大家私下里自己去试吧,这里就不描述了。

那么,咋办呢,网上搜索了半天也没啥好法子,没办法,自己造一下轮子吧。

 

首先整理一下思路,我们想要这样一个方法,命名为

beforeClose()

 

该方法的执行时机是在侧边栏关闭动作发生之前,也就是说,当我们点击了屏幕中白色的部分,此时,不执行系统默认的关闭操作,执行该方法(beforeClose()),也就是将系统的默认关闭操作拦截住,然后,在此方法中去执行我们的业务代码,当执行完我们的业务代码之后,继续执行系统默认的关闭操作,ok,思路顺出来了,我们去解决问题。

第一个问题,先找到系统默认的关闭操作在哪写的。在哪写的呢,我们先来认真看一下系统默认的关闭流程是怎样的,首先,我们点击屏幕上的空白部分,对应的触摸事件应该是    ACTION_DOWN,那么此时手不要着急从屏幕上松开,我们观察此时侧边栏滑动了吗?答案是没有!那么这证明了,在ACTION_DOWN这步操作中,未执行侧边栏关闭事件。接着我们松开手指,此时应该执行 ACTION_UP,那么此时我们看到,哎,侧边栏滑动回去了,这一步证明了,系统默认的关闭方法在  ACTION_UP这一个事件底下,好,我们现在去源码里看一下这部分代码,很容易找到,如下:

 

public boolean onTouchEvent(MotionEvent ev) {
      ......
        switch (action & 255) {
            case ACTION_DOWN:
               ........
                break;
            case ACTION_UP:
                x = ev.getX();
                y = ev.getY();
                boolean peekingOnly = true;
                View touchedView = this.mLeftDragger.findTopChildUnder((int) x, (int) y);
                if (touchedView != null && this.isContentView(touchedView)) {
                    float dx = x - this.mInitialMotionX;
                    float dy = y - this.mInitialMotionY;
                    int slop = this.mLeftDragger.getTouchSlop();
                    if (dx * dx + dy * dy < (float) (slop * slop)) {
                        View openDrawer = this.findOpenDrawer();
                        if (openDrawer != null) {
                            peekingOnly = this.getDrawerLockMode(openDrawer) == 2;
                        }
                    }
                }

                this.closeDrawers(peekingOnly);

                this.mDisallowInterceptRequested = false;
           ..............
        }

        return wantTouchEvents;
    }

 

 

代码较多,无关内容略过,我们只看重要的,那么在   onTouchEvent  的  ACTION_UP动作下面我们发现了这样一个方法,

 this.closeDrawers(peekingOnly);
也就是:
void closeDrawers(boolean peekingOnly) {
boolean needsInvalidate = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

if (!isDrawerView(child) || (peekingOnly && !lp.isPeeking)) {
continue;
}

final int childWidth = child.getWidth();

if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) {
needsInvalidate |= mLeftDragger.smoothSlideViewTo(child,
-childWidth, child.getTop());
} else {
needsInvalidate |= mRightDragger.smoothSlideViewTo(child,
getWidth(), child.getTop());
}

lp.isPeeking = false;
}

mLeftCallback.removeCallbacks();
mRightCallback.removeCallbacks();

if (needsInvalidate) {
invalidate();
}
}

 

那么这个是我们要找的吗 ?这里有这么一句代码:

mLeftDragger.smoothSlideViewTo(..,..,..)

 

看到这个方法名激不激动,翻译一下这个方法名,顺滑的侧边视图to........,是不是很激动?怎么去验证到底是不是它呢?开debug,自己断点验证一下,没错,就是它!!!

 

ok,到这里我们的工作就完成的大半了,剩下来就是去在closeDrawers里去拦截这个方法的调用,然后去执行我们的业务代码,然后再调用这个代码了。

 

有小伙伴可能问了,drawerlayout 是系统的控件,不允许我们更改,我们又不能改代码,怎么拦截啊?呵呵,系统不让我们改,我们还不能自己新建一个类吗,代码直接copy系统的就完事了呗。

所以,就有了下面这个东西:

package com.zhd.life.helloworld2;
........
public class MyDrawerLayout extends ViewGroup {
   public MyDrawerLayout(@NonNull Context context) {
        this(context, (AttributeSet) null);
    }

    public MyDrawerLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyDrawerLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
       
    }

   .......
}

 

我们新建一个类。命名为MyDrawerLayout,然后里面的细节代码直接全部copy系统的,一点不变,这次让改了吧???!!!

好了,现在开始拦截,由于视图的滑动在MyDrawerLayout里面,而我们的业务代码在我们的activity中,那么我们就需要  closeDrawers()方法调用之前,通知我们的activity,通知方式有很多,handler,接口回调,eventbus,都行,在此我们选择接口回调的方式,为什么选择这个呢?我个人习惯,大家习惯用那种方式,大家自己选,废话少说,看代码,

首先定义一个接口如下,

public interface OnClickWiteLisener {
        void beforeClose();
    }

 

在我们的MydrawerLayout 的closeDrawers中这样写,

void closeDrawers(boolean peekingOnly) {
        needsInvalidate = false;
        int childCount = this.getChildCount();
        for (int i = 0; i < childCount; ++i) {
         ...........
            if (this.isDrawerView(child) && (!peekingOnly || lp.isPeeking)) {

                if (mOnClickWiteLisener!=null){
                    mOnClickWiteLisener.beforeClose();
                }else{
                    close();
                }
            }
        }

        this.mLeftCallback.removeCallbacks();
        this.mRightCallback.removeCallbacks();
        if (needsInvalidate) {
            this.invalidate();
        }

    }    

 

 

close方法如下:

   private View child;
    private boolean needsInvalidate = false;
    private MyDrawerLayout.LayoutParams lp;

    public void close() {
        int childWidth = child.getWidth();
        if (this.checkDrawerViewAbsoluteGravity(child, 3)) {
            needsInvalidate |= this.mLeftDragger.smoothSlideViewTo(child, -childWidth, child.getTop());
        } else {
            needsInvalidate |= this.mRightDragger.smoothSlideViewTo(child, this.getWidth(), child.getTop());
        }

        lp.isPeeking = false;
    }

 

这样就完成了我们的拦截功能,当我们在activity中实现了 OnClickWiteLisener 的时候,执行我们自己定义的回调;没有实现的时候,继续系统原来的流程不变,。

在activity中这样使用,

public class MainActivity extends AppCompatActivity implements MyDrawerLayout.OnClickWiteLisener {

    //自定义的MyDrawerLayout
    private MyDrawerLayout mDrawerLayout;

    //点击此按钮打开侧边栏
    private TextView show;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDrawerLayout = findViewById(R.id.aaa);
        show = findViewById(R.id.show);
        mDrawerLayout.setOnClickWiteLisener(this);
        show.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mDrawerLayout.openDrawer(Gravity.END);
            }
        });
    }

    @Override
    public void beforeClose() {
        Toast.makeText(this, "我点击了白屏,想要关闭,我在此执行了保存数据操作,", Toast.LENGTH_SHORT).show();
        mDrawerLayout.close();
    }
}

 

 

ok,大功告成。就这样就可以了。

困了,睡

 

以上是关于自定义DrawerLayout抽屉布局的主要内容,如果未能解决你的问题,请参考以下文章

Android:在drawerlayout中使用地图膨胀片段时出错

DrawerLayout 的项目点击 - 啥时候更换片段合适?

DrawerLayout(抽屉效果)

安卓笔记抽屉式布局----DrawerLayout

AndroidTV - 创建自定义布局

Android - V之DrawerLayout的使用