Android自定义LinearLayout实现左右侧滑菜单,完美兼容ListView、ScrollView、ViewPager等滑动控件
2016-06-14 17:04
1031 查看
国际惯例,先来效果图
在阅读本文章之前,请确定熟悉【Scroller】相关的知识,如果不熟悉,请小伙伴儿先百度后再来吧。
假如你已经知道【Scroller】了,那么就接着往下看吧。
首先,我们把侧拉菜单的构造给解析出来。多次观看上面的效果图,我们可以得出以下的结论。
整体可以看做是一个ViewGroup,这个ViewGroup包含了最多三个子View(分别是左菜单的红色View、中间正文内容的白色View、右菜单的蓝色View);
三个子View(我称为UI界面,因为代码中的Java类就取名这个)的移动是在ViewGroup的onTouchEvent方法中控制;
每个UI界面都拥有独特的东西,比如子控件布局,因此我们希望用R.layout.*的方式引入;
每个UI界面又都拥有相同的属性,比如都有宽度属性,滑动临界值属性,那么就可以用一个超类来封装所有相似的东西;
最最重要的地方,动态计算出scrollX的值,然后用Scroller来滑动。
理清楚了结构后,我们来开始第一步的设计,也就是封装超类,首先给出代码:
这个UI超类就用于模拟每一个界面,其中主要封装了内容View的设置、跳转界面的逻辑代码,以及暴露出去需要子类实现的calculate方法,这个calculate方法主要是要计算startX、stopX、width以及各子类独有的属性。
接下来展示左菜单的实现类LeftMenuUI:
代码那是相当的简洁,在calculate方法中除了计算startX和stopX和width之外,还计算了openX和closeX的值。那么问题来了,此处的openX和closeX的什么东西呢?先看下图所示。
首先黑色框代表的是整个的布局,被分为了三个部分,分别是左菜单、正文内容、右菜单。红色框代表的是手机的屏幕,默认手机屏幕的宽高和正文内容的宽高都是一样的。因此图上所示是重合的。
那么问题来了,途中所示的绿色横线代表的openX和closeX分别是什么意思呢?我们假想一下,我们现在正处于正文的内容,此时手指向右滑屏,将滑出左菜单的部分,此时红框代表的屏幕就会向左移动(如果听不懂就真的需要先了解Scroller的使用哟),如果红色框移动到openX这个绿线的左边,我们就认为超出了滑动的临界值,判断为显示左菜单的操作,现在应该明白了openX的意思了吧,就是超过这个值就显示左菜单。
那么问题又来了,closeX怎么解释呢?我们再次假象一下,我们现在正处于左菜单,此时我们向左滑动屏幕,如果红色框从0开始向右移动,如果超出了closeX这个临界值,就代表我们要滑出左菜单进入正文内容,这就是closeX的意思。
好了,理解了左菜单这个类,那么正文内容和右菜单也同样好理解了。接下来给出正文类:ContentUI
正文菜单更简单,没有openX和closeX的计算,为什么呢?因为左菜单和右菜单的划入划出判断我都放在对应的UI类里面。接下来是RightMenuUI
这个类也同样有openX和closeX的计算,但是大家要特别注意的一点是:右菜单的openX和closeX是在正文菜单的坐标内,要问为什么的话,大家需要了解Scroller的原理并向我一样画一个草图来理解。
最后就是SideLayout这个自定义控件了,其实就只是在onTouchEvent中做了滑动的逻辑判断操作。首先给出源代码:
在ACTION_MOVE操作中,根据移动的偏移量来滑动控件,这里需要特别注意左边的0临界值和右边的measureWidth测量宽度的临界值,不然会滑出屏幕之外哟。最重要的方法还是toTargetUI这个方法,我单独把这个方法剔出来讲解。
首先,根据ACTION_UP传递进来dx参数进入滑动方向的判断,这个非常重要,不同的滑动方向对于处在不同scrollX的控件来说,操作目的是不一样。我们以右滑为例,如果leftMenuUI为空,就代表用户只想要左滑功能,rightMenuUI为空,就代表用户只想要右滑功能。leftMenuUI和rightMenuUI都不为空,表示用户同时想要左滑和右滑的功能。
然后对于不同的坐标区别进行判断,并显示出对应的UI界面。这里文字一时半会儿说不明白。大家把我上面的图拿来对比代码进行分析,一会儿就看明白啦。
还有一个工具方法,是为了得到屏幕宽度的:
OK,自定义控件的源代码就上面这6个文件,接下来讲讲怎么使用。
首先,我们准备三个布局文件,分别代表左菜单布局、正文内容布局和右菜单布局。
left.xml:
content.xml
right.xml
然后我们需要一个activity_main布局用户呈现Activity(这不废话么。。。)
最后一步了,在代码中调用:如果你只想要左菜单,那么就只调用setLeftMenuView,如果只想要右菜单,那么就只调用setRightMenuView,如果左菜单和右菜单都想要,那么就一起调用。(setContentView必须调用哈,别问我为什么
)
好了,讲解完了,接下来是激动人心的时候了,Demo地址~~~
点我去下载Demo哟
在阅读本文章之前,请确定熟悉【Scroller】相关的知识,如果不熟悉,请小伙伴儿先百度后再来吧。
假如你已经知道【Scroller】了,那么就接着往下看吧。
首先,我们把侧拉菜单的构造给解析出来。多次观看上面的效果图,我们可以得出以下的结论。
整体可以看做是一个ViewGroup,这个ViewGroup包含了最多三个子View(分别是左菜单的红色View、中间正文内容的白色View、右菜单的蓝色View);
三个子View(我称为UI界面,因为代码中的Java类就取名这个)的移动是在ViewGroup的onTouchEvent方法中控制;
每个UI界面都拥有独特的东西,比如子控件布局,因此我们希望用R.layout.*的方式引入;
每个UI界面又都拥有相同的属性,比如都有宽度属性,滑动临界值属性,那么就可以用一个超类来封装所有相似的东西;
最最重要的地方,动态计算出scrollX的值,然后用Scroller来滑动。
理清楚了结构后,我们来开始第一步的设计,也就是封装超类,首先给出代码:
/** * Created by ccwxf on 2016/6/14. */ public abstract class UI { protected Context context; //当前UI界面的布局文件 protected View contentView; //当前UI界面在父控件的起点X坐标 protected int startX; //当前UI界面在父控件的终点X坐标 protected int stopX; //当前UI界面的宽度 protected int width; protected UI(Context context, View contentView){ this.context = context; this.contentView = contentView; } protected abstract void calculate(float leftScale, float rightScale); protected void show(Scroller mScroller){ if(mScroller != null){ mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), startX - mScroller.getFinalX(), 0); } } }
这个UI超类就用于模拟每一个界面,其中主要封装了内容View的设置、跳转界面的逻辑代码,以及暴露出去需要子类实现的calculate方法,这个calculate方法主要是要计算startX、stopX、width以及各子类独有的属性。
接下来展示左菜单的实现类LeftMenuUI:
/** * Created by ccwxf on 2016/6/14. */ public class LeftMenuUI extends UI { // 是指要打开该UI界面所需要滚动的X坐标临界值 public int openX; // 是指要关闭该UI界面所需要的滚动的X坐标临界值 public int closeX; public LeftMenuUI(Context context, View contentView) { super(context, contentView); } @Override protected void calculate(float leftScale, float rightScale) { startX = 0; stopX = (int) (Util.getScreenWidth(context) * leftScale); this.width = stopX - startX; this.openX = (int) (startX + (1 - SideLayout.DEFAULT_SIDE) * this.width); this.closeX = (int) (startX + SideLayout.DEFAULT_SIDE * this.width); } }
代码那是相当的简洁,在calculate方法中除了计算startX和stopX和width之外,还计算了openX和closeX的值。那么问题来了,此处的openX和closeX的什么东西呢?先看下图所示。
首先黑色框代表的是整个的布局,被分为了三个部分,分别是左菜单、正文内容、右菜单。红色框代表的是手机的屏幕,默认手机屏幕的宽高和正文内容的宽高都是一样的。因此图上所示是重合的。
那么问题来了,途中所示的绿色横线代表的openX和closeX分别是什么意思呢?我们假想一下,我们现在正处于正文的内容,此时手指向右滑屏,将滑出左菜单的部分,此时红框代表的屏幕就会向左移动(如果听不懂就真的需要先了解Scroller的使用哟),如果红色框移动到openX这个绿线的左边,我们就认为超出了滑动的临界值,判断为显示左菜单的操作,现在应该明白了openX的意思了吧,就是超过这个值就显示左菜单。
那么问题又来了,closeX怎么解释呢?我们再次假象一下,我们现在正处于左菜单,此时我们向左滑动屏幕,如果红色框从0开始向右移动,如果超出了closeX这个临界值,就代表我们要滑出左菜单进入正文内容,这就是closeX的意思。
好了,理解了左菜单这个类,那么正文内容和右菜单也同样好理解了。接下来给出正文类:ContentUI
public class ContentUI extends UI { public ContentUI(Context context, View contentView) { super(context, contentView); } @Override protected void calculate(float leftScale, float rightScale) { int width = Util.getScreenWidth(context); int leftWidth = (int) (width * leftScale); startX = leftWidth; stopX = leftWidth + width; this.width = stopX - startX; } }
正文菜单更简单,没有openX和closeX的计算,为什么呢?因为左菜单和右菜单的划入划出判断我都放在对应的UI类里面。接下来是RightMenuUI
/** * Created by ccwxf on 2016/6/14. */ public class RightMenuUI extends UI { // 是指要打开该UI界面所需要滚动的X坐标临界值 public int openX; // 是指要关闭该UI界面所需要的滚动的X坐标临界值 public int closeX; public RightMenuUI(Context context, View contentView) { super(context, contentView); } @Override protected void calculate(float leftScale, float rightScale) { int width = Util.getScreenWidth(context); startX = (int) (width * (1 + leftScale)); stopX = (int) (width * (1 + leftScale + rightScale)); this.width = stopX - startX; this.openX = (int) (startX - width + SideLayout.DEFAULT_SIDE * this.width); this.closeX = (int) (startX - width + (1 - SideLayout.DEFAULT_SIDE) * this.width); } /** * 必须重载父类方法,因为滑动的起点是从0开始 */ protected void show(Scroller mScroller, int measureWidth){ if(mScroller != null){ mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), measureWidth - Util.getScreenWidth(context) - mScroller.getFinalX(), 0); } } }
这个类也同样有openX和closeX的计算,但是大家要特别注意的一点是:右菜单的openX和closeX是在正文菜单的坐标内,要问为什么的话,大家需要了解Scroller的原理并向我一样画一个草图来理解。
最后就是SideLayout这个自定义控件了,其实就只是在onTouchEvent中做了滑动的逻辑判断操作。首先给出源代码:
/** * Created by ccwxf on 2016/6/14. */ public class SideLayout extends LinearLayout { //默认的菜单宽度与屏幕宽度的比值 public static final float DEFAULT_SCALE = 0.66f; //默认的滑动切换阀值相对于菜单宽度的比值 public static final float DEFAULT_SIDE = 0.25f; private Scroller mScroller; //三个UI界面 private LeftMenuUI leftMenuUI; private ContentUI contentUI; private RightMenuUI rightMenuUI; //左菜单和右菜单相对于屏幕的比值 private float leftScale = 0; private float rightScale = 0; //控件的测量宽度 private float measureWidth = 0; //手指Touch时的X坐标和移动时的X坐标 private float mTouchX; private float mMoveX; public SideLayout(Context context) { super(context); init(); } public SideLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } public SideLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { mScroller = new Scroller(getContext()); setOrientation(LinearLayout.HORIZONTAL); } /** * 设置左菜单的布局 * @param view 左菜单布局 * @return 返回当类 */ public SideLayout setLeftMenuView(View view){ return setLeftMenuView(view, DEFAULT_SCALE); } public SideLayout setLeftMenuView(View view, float leftScale){ leftMenuUI = new LeftMenuUI(getContext(), view); this.leftScale = leftScale; return this; } /** * 设置右菜单的布局 * @param view 右菜单布局 * @return 当类 */ public SideLayout setRightMenuView(View view){ return setRightMenuView(view, DEFAULT_SCALE); } public SideLayout setRightMenuView(View view, float rightScale){ rightMenuUI = new RightMenuUI(getContext(), view); this.rightScale = rightScale; return this; } /** * 设置正文布局 * @param view 正文布局 * @return 返回当类 */ public SideLayout setContentView(View view){ contentUI = new ContentUI(getContext(), view); return this; } /** * 提交配置,必须调用 */ public void commit() { removeAllViews(); if(leftMenuUI != null){ leftMenuUI.calculate(leftScale, rightScale); measureWidth += leftMenuUI.width; addView(leftMenuUI.contentView, new LayoutParams(leftMenuUI.width, LayoutParams.MATCH_PARENT)); } if(contentUI != null){ contentUI.calculate(leftScale, rightScale); measureWidth += contentUI.width; addView(contentUI.contentView, new LayoutParams(contentUI.width, LayoutParams.MATCH_PARENT)); } if(rightMenuUI != null){ rightMenuUI.calculate(leftScale, rightScale); measureWidth += rightMenuUI.width; addView(rightMenuUI.contentView, new LayoutParams(rightMenuUI.width, LayoutParams.MATCH_PARENT)); } } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: mTouchX = event.getX(); mMoveX = event.getX(); return true; case MotionEvent.ACTION_MOVE: int dx = (int) (event.getX() - mMoveX); if(dx > 0){ //右滑 if(mScroller.getFinalX() > 0){ mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -dx, 0); }else{ mScroller.setFinalX(0); } }else{ //左滑 if(mScroller.getFinalX() + Util.getScreenWidth(getContext()) - dx < measureWidth){ mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -dx, 0); }else{ mScroller.setFinalX((int) (measureWidth - Util.getScreenWidth(getContext()))); } } mMoveX = event.getX(); invalidate(); break; case MotionEvent.ACTION_UP: toTargetUI((int) (event.getX() - mTouchX)); break; } return super.onTouchEvent(event); } /** * 滑动切换到目标的UI界面 * @param dx 手指抬起时相比手指落下,滑动的距离 */ private void toTargetUI(int dx){ int scrollX = mScroller.getFinalX(); if(dx > 0){ //右滑 if(leftMenuUI != null){ if(scrollX >= leftMenuUI.openX && scrollX < leftMenuUI.stopX){ contentUI.show(mScroller); }else if(scrollX >= leftMenuUI.startX && scrollX < leftMenuUI.openX){ leftMenuUI.show(mScroller); } } if(rightMenuUI != null){ if(scrollX >= rightMenuUI.closeX){ rightMenuUI.show(mScroller, (int) measureWidth); }else if(scrollX >= contentUI.startX && scrollX < rightMenuUI.closeX){ contentUI.show(mScroller); } } }else{ //左滑 if(leftMenuUI != null){ if(scrollX > leftMenuUI.startX && scrollX <= leftMenuUI.closeX){ leftMenuUI.show(mScroller); }else if(scrollX > leftMenuUI.closeX && scrollX < leftMenuUI.stopX){ contentUI.show(mScroller); } } if(rightMenuUI != null){ if(scrollX > contentUI.startX && scrollX <= rightMenuUI.openX){ contentUI.show(mScroller); }else if(scrollX > rightMenuUI.openX){ rightMenuUI.show(mScroller, (int) measureWidth); } } } } @Override public void computeScroll(){ if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } super.computeScroll(); } }
在ACTION_MOVE操作中,根据移动的偏移量来滑动控件,这里需要特别注意左边的0临界值和右边的measureWidth测量宽度的临界值,不然会滑出屏幕之外哟。最重要的方法还是toTargetUI这个方法,我单独把这个方法剔出来讲解。
/** * 滑动切换到目标的UI界面 * @param dx 手指抬起时相比手指落下,滑动的距离 */ private void toTargetUI(int dx){ int scrollX = mScroller.getFinalX(); if(dx > 0){ //右滑 if(leftMenuUI != null){ if(scrollX >= leftMenuUI.openX && scrollX < leftMenuUI.stopX){ contentUI.show(mScroller); }else if(scrollX >= leftMenuUI.startX && scrollX < leftMenuUI.openX){ leftMenuUI.show(mScroller); } } if(rightMenuUI != null){ if(scrollX >= rightMenuUI.closeX){ rightMenuUI.show(mScroller, (int) measureWidth); }else if(scrollX >= contentUI.startX && scrollX < rightMenuUI.closeX){ contentUI.show(mScroller); } } }else{ //左滑 if(leftMenuUI != null){ if(scrollX > leftMenuUI.startX && scrollX <= leftMenuUI.closeX){ leftMenuUI.show(mScroller); }else if(scrollX > leftMenuUI.closeX && scrollX < leftMenuUI.stopX){ contentUI.show(mScroller); } } if(rightMenuUI != null){ if(scrollX > contentUI.startX && scrollX <= rightMenuUI.openX){ contentUI.show(mScroller); }else if(scrollX > rightMenuUI.openX){ rightMenuUI.show(mScroller, (int) measureWidth); } } } }
首先,根据ACTION_UP传递进来dx参数进入滑动方向的判断,这个非常重要,不同的滑动方向对于处在不同scrollX的控件来说,操作目的是不一样。我们以右滑为例,如果leftMenuUI为空,就代表用户只想要左滑功能,rightMenuUI为空,就代表用户只想要右滑功能。leftMenuUI和rightMenuUI都不为空,表示用户同时想要左滑和右滑的功能。
然后对于不同的坐标区别进行判断,并显示出对应的UI界面。这里文字一时半会儿说不明白。大家把我上面的图拿来对比代码进行分析,一会儿就看明白啦。
还有一个工具方法,是为了得到屏幕宽度的:
/** * Created by ccwxf on 2016/6/14. */ public class Util { public static int getScreenWidth(Context context){ return context.getResources().getDisplayMetrics().widthPixels; } }
OK,自定义控件的源代码就上面这6个文件,接下来讲讲怎么使用。
首先,我们准备三个布局文件,分别代表左菜单布局、正文内容布局和右菜单布局。
left.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_red_light" android:orientation="vertical"> <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="wrap_content" android:listSelector="@android:color/transparent" android:cacheColorHint="@android:color/transparent" android:dividerHeight="0dp" android:divider="@null" /> </LinearLayout>
content.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" android:orientation="vertical"> <android.support.v4.view.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="300dp"/> </LinearLayout>
right.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_blue_light" android:orientation="vertical"> <HorizontalScrollView android:layout_width="match_parent" android:layout_height="wrap_content" > <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="horizontal" > <TextView android:layout_width="300dp" android:layout_height="300dp" android:background="@android:color/holo_green_light" /> <TextView android:layout_width="300dp" android:layout_height="300dp" android:background="@android:color/darker_gray" /> <TextView android:layout_width="300dp" android:layout_height="300dp" android:background="@android:color/holo_orange_light" /> </LinearLayout> </HorizontalScrollView> </LinearLayout>
然后我们需要一个activity_main布局用户呈现Activity(这不废话么。。。)
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <cc.wxf.side.SideLayout android:id="@+id/sideLayout" android:layout_width="match_parent" android:layout_height="match_parent"/> </RelativeLayout>
最后一步了,在代码中调用:如果你只想要左菜单,那么就只调用setLeftMenuView,如果只想要右菜单,那么就只调用setRightMenuView,如果左菜单和右菜单都想要,那么就一起调用。(setContentView必须调用哈,别问我为什么
)
public class MainActivity extends Activity { private View leftView; private View contentView; private View rightView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initUI(); SideLayout sideLayout = (SideLayout) findViewById(R.id.sideLayout); sideLayout.setLeftMenuView(leftView).setContentView(contentView).setRightMenuView(rightView).commit(); } private void initUI(){ leftView = View.inflate(this, R.layout.left, null); contentView = View.inflate(this, R.layout.content, null); rightView = View.inflate(this, R.layout.right, null); //初始化左边菜单 ListView listView = (ListView) leftView.findViewById(R.id.listView); listView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, new String[]{ "123","456","789","101","112","123","456","789","101","112","123","456","789","101","112","123","456","789","101","112" })); //初始化正文内容 ViewPager viewPager = (ViewPager) contentView.findViewById(R.id.viewPager); viewPager.setAdapter(new TestDemoAdapter()); } public class TestDemoAdapter extends PagerAdapter{ private ImageView[] imageViews = new ImageView[5]; public TestDemoAdapter() { for(int i = 0; i < imageViews.length; i++){ imageViews[i] = new ImageView(MainActivity.this); imageViews[i].setImageResource(R.mipmap.ic_launcher); } } @Override public int getCount() { return 5; } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public Object instantiateItem(ViewGroup container, int position) { container.addView(imageViews[position]); return imageViews[position]; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView(imageViews[position]); } } }
好了,讲解完了,接下来是激动人心的时候了,Demo地址~~~
点我去下载Demo哟
相关文章推荐
- Android Studio 第一次配置及其使用
- Android中ServiceManager的功能
- 关于AndroidStudio升级到2.0的一些问题汇总
- 去除android或者iOS系统默认的一些样式总结
- android 更改api版本
- Android.mk
- imx6 android背光控制
- Android之Fragment(三):动态替换碎片
- Android studio 开发百度地图
- mei yan xiao guo for android
- Android综合项目乐学成语(二)
- Android图片下载缓存库picasso解析
- Android Loader异步装载
- Android入门:Activity四种启动模式
- android自定义控件(一) 入门
- Android 无限循环ViewPager报错跳坑。
- FragmentTabHost类实现android中Fragment的动态加载
- android studio命令打包(gradle)
- Android 对ScrollView滚动监听,实现美团、大众点评的购买悬浮效果
- android悬浮窗口的实现