自定义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中使用地图膨胀片段时出错