有一种控件需求,通过上下滑动来打开上下菜单。这个控件要求自动打开上下两个菜单,而且还要随着手势(注意:多触点)上下滑动菜单。之前Android系统有提供一个叫SlidingDrawer(完整路径:android.widget.SlidingDrawer)的控件,似乎有类似的效果,但是它不仅过时了,而且远远达不到我提出的要求。为了满足这个需求,我设计了一款自名为SlidingDrawerLayout的控件。我们可以先来看看我做的这个控件的效果,读者再考虑是否需要往下看(没能展示多触点功能,有兴趣的童鞋可以下载源码自行编译运行)。效果图接下来,我将采用自定义ViewGroup的方式来实现这一控件。先说思路,再解说关键代码,最后描述使用方法,并给出github源码地址。另外,我们都知道自定义控件,计算的话大都不会太简单,有些甚至很繁琐,这个控件也有比较大的计算量,我也就不在博客中过多的介绍计算细节了(感兴趣的童鞋可以查看源码)。
SlidingDrawerLayout直接继承于ViewGroup,它有3个直接子控件。分别是内容子控件,顶部菜单子控件和底部菜单子控件。顶部子菜单的底部和底部子菜单的顶部都有一个Tab块,方便用户按住后滑动,也可以在Tab中适当添加些与当前子菜单相关的信息。如上图所示,红线区域的高度为SlidingDrawerLayout控件内容叠加的总高度(看懂的同学可以无视接下来的这段话)。原始模型中,有3个与SlidingDrawerLayout等宽等高的矩形(假设高为H),深蓝色矩形代表顶部子菜单,灰色矩形代表内容子控件,浅蓝色矩形代表底部子菜单。假设把他们按“顶部、内容、底部”的顺序放在一个类似垂直的线性布局中,并且把内容子控件上下移动,调整到内容子控件完全显示的位置,那么顶部子菜单和底部子菜单均不可见。为了实现我们的需求,即顶部、底部菜单都可以按住滑动,我们可以将顶部子菜单往下移动一段距离TopTab_H,将底部子菜单往上移动一段距离BottomTab_H,内容子菜单先保持不动。现在每个子控件的高度仍然没有发生变化,细心的读者会发现,此时的内容子控件的顶部和底部都被遮住了一部分,这会使得显示在内容子控件顶部和底部的内容被遮住。那怎么办呢?这时候我们要改变内容子控件的高度了,即Content_H = H - TopTab_H - BottomTab_H,很明显这样子内容子控件就不会被遮住了。还没完,如果按照现在的样子,我们打开顶部菜单,顶部菜单下滑而填满了整个屏幕,底部菜单也是如此。这看起来好像不是很爽是吧?如果我打开了顶部菜单,又想打开底部菜单的话,非得先把顶部菜单关闭才行,因为底部菜单的Tab被遮住了,我们无法直接按住滑动。为了解决这个问题,我们把顶部子菜单的高度设置为Top_H = H - BottomTab_H,这样打开顶部子菜单的时候,底部Tab恰好完全显示出来。而底部子菜单的高度也是同理,即Bottom_H = H - TopTab_H。
事件处理是实现这个控件最关键的地方之一,SlidingDrawerLayout事件处理大致上比较简单,但是计算和判断类操作的实现上确实比较让人头疼。下面我从2个方面来简单表述下处理思路,详细情况请看代码解析部分或者源码。一方面是事件拦截处理,即对SlidingDrawerLayout的onInterceptTouchEvent方法的处理(对事件拦截没有了解的童鞋请自行脑补)。我们先用2个ArrayMap分别存储每一个触点“上一次”的事件触发位置的(x, y)坐标,在move事件中循环遍历每一个触点,先判断每一个触点的“上一个”位置是否落在顶部或底部Tab里边,如果是的话,再看比较垂直方向和水平方向的偏移量大小,当垂直方向的偏移量明显大于水平方向的偏移量时,我们认为当前这个触点的move事件应该交由SlidingDrawerLayout来处理,因此我们要拦截这个事件。当然,我们还要标记好到底是选中了顶部还是底部,这个就确定了滑动对象。这就是SlidingDrawerLayout事件拦截处理的思路。另一方面是对触摸事件的响应,即对SlidingDrawerLayout的onTouchEvent方法的处理。对于触摸事件的处理,我们主要关注在move事件上。在这里我也用ArrayMap来存储了每一个触点“上一次”的纵坐标,方便计算滑动的偏移量。当move事件到来时,循环遍历所有触点,选取一个触点作为参考,用于计算偏移量和调用随手势滑动的方法。这里要注意,即便是选取了一个触点作为参考,仍然要记录每一个触点“上一次”的纵坐标,因为我们做的是多触点事件处理,在任意一次滑动中选取的参考触点可能不一样。当全部手指松开时,就要启动松开滑动机制,这是下一小节的内容。
这部分内容指的是,当我们触碰SlidingDrawerLayout松开后onTouchEvent的up事件要处理的事情,这个也是实现SlidingDrawerLayout很关键的一步,它直接影响了用户体验。松开滑动集成的方法在这个控件中有2个用途,一个是为控件内部的松开滑动提供调用,另一个是给使用者外部调用,比我你点击一个按钮,菜单会打开或者关闭等。松开滑动的实现思路非常简单,无非就是当onTouchEvent方法的up事件到来时,调用自动打开或者关闭菜单的方法,这些方法负责根据手势的速度平滑地滑动菜单。不过,在实现的细节上也跟事件处理一样,计算会比较多。
代码解析这部分,我分为四个部分,分别是外部参数的传入,布局初始化,事件处理和对用户开放的方法。其中最需要注意的是事件处理部分,其次是布局的初始化,弄懂这两个部分就掌握了这个控件的功能运行机制了。
这个控件目前开放的外部参数传入方法只有3个。其中2个是设置上下子菜单高度的方法,就是给用户按住滑动的那2片区域的高度。另外一个是对SlidingDrawerLayout子控件的传入,总共有3个子控件可以传入,分别是上、下子菜单布局和内容布局,而这个几个参数按照我的设计是写在布局文件中的。
设置上下子菜单高度的方法,如下所示。这两个方法都有一个isPx的参数,询问用户是否以像素为单位传入,如果不是则内部会认为这是以dp为单位的值,然后对传入的值做换算。
/*** Set the top tab height.* * @param value* @param isPx* Whether using pixel for unit.*/public void setTopTabHeight(int value, boolean isPx) {if (isPx) {mTopTabHeight = value;} else {mTopTabHeight = getTabHeight(value);}}/*** Set the bottom tab height.* * @param value* @param isPx* Whether using pixel for unit.*/public void setBottomTabHeight(int value, boolean isPx) {if (isPx) {mBottomTabHeight = value;} else {mBottomTabHeight = getTabHeight(value);}}
首先把子布局布局赋值给SlidingDrawerLayout里的成员变量,这部分在onFinishInflate方法完成。
@Overrideprotected void onFinishInflate() {FinishInflate();// Find child.View topView = findView("topView");View contentView = findView("contentView");View bottomView = findView("bottomView");if (topView != null) {topView.setClickable(true);// Default size.setTopTabHeight(50, false);mTopView = topView;}if (contentView != null) {mContentView = contentView;}if (bottomView != null) {bottomView.setClickable(true);setBottomTabHeight(50, false);mBottomView = bottomView;}}
然后,是要测量出上下子菜单和内容的高度,这部分在onMeasure方法完成。我们只需要根据实现思路的控件结构部分描述的那样,计算出各个子控件高度即可。
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int specHeight = Size(heightMeasureSpec);// Initialise top height.if (mTopView != null) {int topHeightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight- mBottomTabHeight, MeasureSpec.EXACTLY);measureChild(mTopView, widthMeasureSpec, topHeightMeasureSpec);}// Initialise content height.if (mContentView != null) {int contentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight - mTopTabHeight - mBottomTabHeight,MeasureSpec.EXACTLY);measureChild(mContentView, widthMeasureSpec,contentHeightMeasureSpec);}// Initialise bottom heightif (mBottomView != null) {int bottomHeightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight - mTopTabHeight, MeasureSpec.EXACTLY);measureChild(mBottomView, widthMeasureSpec, bottomHeightMeasureSpec);}Measure(widthMeasureSpec, heightMeasureSpec);}
最后,是要把3个子控件安放在SlidingDrawerLayout的哪个位置,这是在onLayout方法中完成的,也是按照控件结构部分思路来实现的。
@Overrideprotected void onLayout(boolean changed, int left, int top, int right,int bottom) {// Initialise top position.if (mTopView != null) {int t = -(getMeasuredHeight() - mTopTabHeight - mBottomTabHeight);int b = mTopTabHeight;mTopView.layout(0, t, getMeasuredWidth(), b);}// Initialise content position.if (mContentView != null) {int t = mTopTabHeight;int b = getMeasuredHeight() - mBottomTabHeight;mContentView.layout(0, t, getMeasuredWidth(), b);}// Initialise bottom position.if (mBottomView != null) {int t = getMeasuredHeight() - mBottomTabHeight;int b = getMeasuredHeight() + (getMeasuredHeight() - mTopTabHeight);mBottomView.layout(0, t, getMeasuredWidth(), b);}}
事件处理的处理逻辑主要体现在onInterceptTouchEvent方法和onTouchEvent方法上,事件拦截是为了使用户在选中菜单Tab的时候把事件交于SlidingDrawerLayout处理,触摸方法就是处理分发下来的事件,并作出相应的动作。所以,下面我主要说一下这两个方法的代码实现。
用于事件拦截onInterceptTouchEvent方法中,当down和pointer_down事件触发时,收集好所有触点按下的(x, y)坐标。接着,move事件触发时,遍历所有触点,先获取每一触点的当前坐标和“上一次”坐标,然后求出偏移量,最后看“上一次”坐标是否落在某个Tab区域和垂直偏移量是否明显大于水平偏移量(代码中指dx < dy - 5),如果同时满足这2个条件的话,那么这个事件就要拦截,也说明选中了哪一个菜单,并且做好标记。最后,up事件触发时,我在这里做了一些标记的重置操作。
@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {int action = Action();switch (action & MotionEvent.ACTION_MASK) {case MotionEvent.ACTION_DOWN:mLastXForIntercept.clear();mLastYForIntercept.clear();// Record the first point.mLastXForIntercept.PointerId(0), X());mLastYForIntercept.PointerId(0), Y());break;case MotionEvent.ACTION_POINTER_DOWN:mLastXForIntercept.clear();mLastYForIntercept.clear();// Record all points.for (int i = 0; i < PointerCount(); i++) {int id = PointerId(i);float x = X(i);float y = Y(i);mLastXForIntercept.put(id, x);mLastYForIntercept.put(id, y);}break;case MotionEvent.ACTION_MOVE:// Check all points.for (int i = 0; i < PointerCount(); i++) {int id = PointerId(i);float x = X(i);float y = Y(i);float lastX = (id);float lastY = (id);float dx = Math.abs(x - lastX);float dy = Math.abs(y - lastY);// Check top view.if (mTopView != null) {float topY = Y();float topTabY = topY + Height() - mTopTabHeight;if (lastY >= topTabY && lastY <= topTabY + mTopTabHeight) {if (dx < dy - 5) {mLastY.clear();mLastY.put(id, y);mSelectedTop = true;// Judge again.if (!shouldIntercept(false, true)) {return false;}return true;}}}// Check Bottom view.if (mBottomView != null) {float bottomY = Y();if (lastY >= bottomY && lastY <= bottomY + mBottomTabHeight) {if (dx < dy - 5) {mLastY.clear();mLastY.put(id, y);mSelectedBottom = true;// Judge again.if (!shouldIntercept(true, false)) {return false;}return true;}}}// Record last values.mLastXForIntercept.put(id, x);mLastYForIntercept.put(id, y);}break;case MotionEvent.ACTION_UP:mIsInBackEvent = false;// ResetmSelectedTop = false;mSelectedBottom = false;break;}InterceptTouchEvent(event);}
事件分发给onTouchEvent方法后,我们就要让菜单做出响应了。首先被选中的菜单得要随着手势滑动,这个由slideTop和slideBottom方法来完成。然后,松开手的时候菜单要自动打开或关闭,这是由smoothSlide方法来完成的。关于这几个slide方法的具体实现,我就不在博客中描述了,里面很多计算,太繁琐的话不太好表述,感兴趣的读者自行阅读源码。
从下面的代码中可以看出,我们先把事件添加到速度追踪器中,在后面松开手后要自动滑动的时候,需要从它里面取出当时的速度。
再来看move事件中的逻辑,我似乎只是遍历了所有触点,但仔细看你会发现我的这句代码:if (ainsKey(id) && move) 表示的是只取了一个触点来计算偏移量,为什么呢?因为假设我们每一个触点都计算偏移量,那一次滑动触发将会是所有偏移量累加的结果,那样会滑很远距离的,看起来就不像自己滑的,我们人在感觉这个滑动是把注意力放在了一个点上,这才感觉符合自己的生活经验。那有的同学会说,我们取一个点不就可以了吗?干嘛还循环,注意循环体的最后一句mLastY.put(id, y);,我们要知道,虽然只取了一个触点作为参考,但每一个触点仍然要记录好坐标,因为在这种多触点事件中我们不能保证每一次取的参考都是同一个。
对于up事件,有两种情况。一种是pointer_up事件,也就是多触点弹起事件,当触发这个事件时,表示某些个触点弹起了,这时我们要把它从“上一次”的坐标记录中删除。另外一种是单纯up事件,这时候表示用户的手已经彻底离开屏幕,我们要启动松开滑动操作了,smoothSlide();就实现了这个功能。当然,还要记得在up事件中做好重置操作。
@Overridepublic boolean onTouchEvent(MotionEvent event) {addVelocityTracker(event);int action = Action();switch (action & MotionEvent.ACTION_MASK) {case MotionEvent.ACTION_MOVE:boolean move = true;for (int i = 0; i < PointerCount(); i++) {int id = PointerId(i);float y = Y(i);if (ainsKey(id) && move) {float lastY = (id);float distance = y - lastY;// Slide tab.if (mSelectedTop) {slideTop((int) distance);}if (mSelectedBottom) {slideBottom((int) distance);}// If slided this time.if (distance != 0) {move = false;}}mLastY.put(id, y);}break;case MotionEvent.ACTION_POINTER_UP:for (int i = 0; i <= ActionIndex(); i++) {int id = PointerId(i);if (ainsKey(id)) {ve(id);}}break;case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:mLastY.clear();smoothSlide();// ResetmSelectedTop = false;mSelectedBottom = false;break;}TouchEvent(event);}
对外开放的方法中分为两个部分,一个是顶部菜单对外开放的方法,另一个是底部菜单对外开放的方法。顶部菜单有3个方法,分别是isTopOpened、openTop、openTopSync、closeTop。底部菜单也有3个方法,分别是isBottomOpened、openBottom、openBottomSync、closeBottom。这些方法从字面意思都知道,无非是判断打开或关闭了菜单,打开或者关闭菜单。在这里,要提一下的是openTopSync和openBottomSync,它们通常是同时使用,因为这两个方法的执行时同步的,这样保证了上下菜单不会交叉打开。
/*** Close the top view.*/public void openTop() {if (mTopView != null) {float startY = Y();openCloseTop(startY, true);}}/*** Close the top view by sync.*/public void openTopSync() {open(true, false);}/*** Close the top view.*/public void closeTop() {if (mTopView != null) {float startY = Y();openCloseTop(startY, false);}}/*** Whether top view opened.* * @return*/public boolean isTopOpened() {if (mTopView != null) {float startY = Y();return startY == 0;}return false;}/*** Open the bottom view.*/public void openBottom() {if (mBottomView != null) {float startY = Y();openCloseBottom(startY, true);}}/*** Open the bottom view by sync.*/public void openBottomSync() {open(false, true);}/*** Close the bottom view.*/public void closeBottom() {if (mBottomView != null) {float startY = Y();openCloseBottom(startY, false);}}/*** Whether bottom view opened.* * @return*/public boolean isBottomOpened() {if (mBottomView != null) {float startY = Y();return startY == mTopTabHeight;}return false;}
至此,我们的上下拉SlidingDrawerLayout就算完成了。总结下,实现这个控件首先要设计好控件的结构,比如宽高、如何摆放等,然后是要处理好onInterceptTouchEvent和onTouchEvent这两个方法,还有一个比较重要的是松开手之后要实现自动滑动,最后是设计好对外开放的接口或方法。
知道如何实现这个控件之后,我再来讲讲如何使用这个控件,这个也是很多童鞋关注的地方吧。按照我的设计,只需要把该控件置于xml布局中作为父容器,然后往里面添加3个子布局作为内容,然后在Java代码中调用对外开放的方法就可以了。首先,我们把SlidingDrawerLayout放在xml布局中作为一个父容器,向里边添加一个内容布局,id设置为contentView,然后再向引入菜单布局,顶部菜单布局id为topView,而底部菜单布局id为bottomView。这里要注意,内容布局一定要有,但不能只有内容布局,那样的话这个控件也没有意义,还有顶部和底部菜单可以任选,可以引入一个菜单,也可以引入两个,当然没必要不给菜单对吧?每个子布局的id一定要按要求设置,不然找不到控件的。
<RelativeLayout xmlns:android=""android:layout_width="match_parent"android:layout_height="match_parent" ><com.slidingdrawerlayout.view.SlidingDrawerLayoutandroid:id="@+id/slidingDrawer"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/content_bg" ><includeandroid:id="@+id/contentView"layout="@layout/content" /><includeandroid:id="@+id/bottomView"layout="@layout/bottom" /><includeandroid:id="@+id/topView"layout="@layout/top" /></com.slidingdrawerlayout.view.SlidingDrawerLayout></RelativeLayout>
最后,我们再来看看Java代码如何使用。先从布局获取到SlidingDrawerLayout控件,再设置它的菜单Tab高度,接下来开发人员可以自由设置打开或关闭的逻辑。
public class MainActivity extends Activity implements OnClickListener {private SlidingDrawerLayout mSlidingDrawer;private View mTopBtn, mBottomBtn;@Overrideprotected void onCreate(Bundle savedInstanceState) {Create(savedInstanceState);setContentView(R.layout.activity_main);findView();initView();}private void findView() {mSlidingDrawer = (SlidingDrawerLayout) findViewById(R.id.slidingDrawer);mTopBtn = findViewById(pBtn);mBottomBtn = findViewById(R.id.bottomBtn);}private void initView() {Resources res = getResources();int topBarSize = (int) Dimension(pBarSize);int bottomBarSize = (int) Dimension(R.dimen.bottomBarSize);mSlidingDrawer.setTopTabHeight(topBarSize, true);mSlidingDrawer.setBottomTabHeight(bottomBarSize, true);mTopBtn.setOnClickListener(this);mBottomBtn.setOnClickListener(this);}@Overridepublic void onClick(View v) {if (v.getId() == pBtn) {if (mSlidingDrawer.isBottomOpened()) {mSlidingDrawer.closeBottom();} else {if (mSlidingDrawer.isTopOpened()) {mSlidingDrawer.closeTop();} else {mSlidingDrawer.openTopSync();}}} else if (v.getId() == R.id.bottomBtn) {if (mSlidingDrawer.isTopOpened()) {mSlidingDrawer.closeTop();} else {if (mSlidingDrawer.isBottomOpened()) {mSlidingDrawer.closeBottom();} else {mSlidingDrawer.openBottomSync();}}}}
}
这是SlidingDrawerLayout的github地址:GitHub - xu0425/SlidingDrawerLayout(欢迎下载,记得给颗星哈!)
以上就是SlidingDrawerLayout的全部内容,这是我设计的一个用于实现上下滑动菜单的控件。有问题的朋友可以评论留言,给我点赞也是不会拒绝的哦!
第一次写博客,心情有点小激动!
本文发布于:2024-02-04 21:26:00,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170716786859755.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |