大家好,欢迎来到IT知识分享网。
概述
侧边栏是一种常见的 UI 结构,用户使用手指左滑或者右滑,可以像抽屉一样拉出隐藏在屏
幕边界之外的内容,既能增加 App 的 UI 的内容,又能给用户带来更新鲜的用户体验,网易新闻、(如图所示)等主流 App 都有类似的设计。
Ø 手指从左侧边沿滑动手指,侧边栏滑出,如果在屏幕中间滑动不起作用;
Ø 在屏幕任意处向左滑动,可以关闭侧边栏;
Ø 侧边栏滑出后,松开手指,侧边栏继续滑动到指定位置;
Ø 支持分割线;
Ø 支持侧边栏宽度设置;
Ø 支持手指滑动感应宽度设置;
Ø 提供直接打开或关闭的功能接口。
使用二进制保存标识数据
位运算符
对于那些“是”或“否”的数据,可以使用位来保存,在 Java 中,一个 int 类型的数据占 4 个字
节,也就是 32 位,一位就能代表一种信息,前面学习到的调用 onMeasure()方法对自定义组件进行尺寸测量时,参数 widthMeasureSpec 和 heightMeasureSpec 中前 2 位保存了尺寸模式,后 30位保存了尺寸大小就是一种典型的位应用。再比如在本章的侧边栏组件中是否显示侧边栏的分割线,为 1 表示为显示,为 0 表示为不显示;侧边栏的打开状态也可以用一位表示,为 1 时表示已打开,为 0 时表示隐藏。我们使用最后一位来表示是否显示分割线,使用倒数第二位来表示侧边栏的打开和关闭状态。示意图如图所示。
所有编程语言的位操作运算符都大同小异,本章不打算将位操作讲得多么完整和透彻(但阅
读后您将会有非常细致的理解),只向您介绍将会用的位操作运算符,主要有:
Ø 按位与(&)
Ø 按位或(|)
Ø 按位取反(~)
Ø 左位移(<<)
Ø 右位移(>>、>>>)
按位与(&):两个二进制值按位与运算,逢 0 则 0。即:1 & 1 = 1、1 & 0 = 0、0 & 1 = 0、0
& 0 = 0;
按位或(|):两个二进制值按位或运算,逢 1 则 1。即:1 | 1 = 1、1 | 0 = 1、0 | 1 = 1、0 |
0 = 0;
按位取反(~):两个二进制值按位取反运算,0 取反为 1,1 取反为 0。即:~0 = 1、~1 =
0;
左位移(<<):将二进制数向左移动若干位,最低位补 0。比如 0000 1000 向左移 2 位,变
成 0010 0000;
右位移(>>):带符号右位移,负数最高位补 1,正数补 0。比如 0000 1000 向右移 2 位,
变成 0000 0010;1111 0000 向右移 2 位(以 1 开头即为负数),变成 1111 1100;
右位移(>>>):不带符号右位移,负数和正数高位都补 0,比如 1111 0000 向右移 2 位,
变成 0011 1100。
对于移位运算符来说,最好使用完整的位进行运算,这样的结果才精确。同时,对于二进制
来说,最高位代表符号,如果最高位为 1 则表示负数,为 0 则表示正数。
我们通过几个案例来演示二进制的运算规则,如果以前概念比较模糊请在这个小节中理解
透。
案例 1:10 & -4 = ?
下面整理出-4 转换成二进制的过程:
1. 先取-4 的绝对值,结果为 4
2. 得到 4 的 2 进制值:0000 0000 0000 0000 0000 0000 0000 0100
3. 将 2 进制的值取反:1111 1111 1111 1111 1111 1111 1111 1011
4. 将第 3)步的结果+1:1111 1111 1111 1111 1111 1111 1111 1100
10 和-4 都是十进制,先将十进制转换成二进制后,再进行 10 & -4 的计算,过程如下:
根据上面的结果得知, 10 & -4 的结果为 8。可以写一个 Java Application 对该结果进行验证。Integer 有一个静态方法 toBinaryString(int n)能将参数 n 以二进制格式返回。
案例 2:10 | -4 = ?
为了简单起见,我们不再使用新的数字,只是改了一下运算符,这次我们计算 10 和-4 按位
或运算之后的结果,前面已经知道了 10 和-4 的二进制表示,我们通过公式计算如下:
案例 1 的计算结果以 0 开头,说明是正数,无需再计算换算。但案例 2 的计算结果是以 1 开
头,说明是负数,负数的二进制如何转换成十进制呢?运算规则其实是相反的:减 1 取反加负
号。我们的演算过程如下:
1111 1111 1111 1111 1111 1111 1111 1110
减 1-> 1111 1111 1111 1111 1111 1111 1111 1101
取反-> 0000 0000 0000 0000 0000 0000 0000 0010 -> 2
取反后结果为 2,加个负号,即为-2,也就是说,10 | -4 = -2。
案例 4:-4 >> 4 = ?
本案例演示带符号右移位运算,-4 向右移 4 位,因为是负数,最高位补 1。即 1111 1111 1111
1111 1111 1111 1111 1100 向右移动 4 位后,结果为:1111 1111 1111 1111 1111 1111 1111 1111,每一位都是 1 了,转换成 10 进制又是多少呢?我们演算一下:
负数-> 1111 1111 1111 1111 1111 1111 1111 1111
减 1-> 1111 1111 1111 1111 1111 1111 1111 1110
取反-> 0000 0000 0000 0000 0000 0000 0000 0001 -> 1
取反后为 1,加负数即为-1,也就是说-4 >> 4 = -1。
位运算的常用功能
对于每一个“位”来说,主要的操作有置 1、置 0 和判断是否为 1 等三个操作。如果使用一
个“位”表示“有”或“无”的概念,则一般来说,1 表示有,0 表示无。假设使用 int 型的最后一位
表示“是一位帅哥”,倒数第二位表示“还是一个小鲜肉”,则:
0000 0000 0000 0000 0000 0000 0000 0011 表示是小鲜肉又是帅哥
0000 0000 0000 0000 0000 0000 0000 0001 表示不是小鲜肉是帅哥
0000 0000 0000 0000 0000 0000 0000 0000 表示不是小鲜肉也不是帅哥
0000 0000 0000 0000 0000 0000 0000 0010 表示是小鲜肉但不是帅哥
上述案例中,程序在运行时,可能要对后面的两位进行变值操作,换句话说,需要将某一位
进行置 0 或置 1 的操作,不同的“位”需要和不同的值进行位运算,如果是第 n 位(从低位算
起,n 从 0 开始),则该值为 2 的 n 次方,如图所示。
上述中的 2 的 n 次方其实是有章可循的,比如是 2 0 是 1,2 1 是 10,2 2 是 100,2 3 是 1000,2 4 是 10000……依此类推。
如果有个 int 类型的数 N,现针对第 n 位进行置 0、置 1 和判断是否为 1 的操作,规则如下:
Ø 置 0:N & ~ 2 n
Ø 置 1:N | 2 n
Ø 判断是否为 1:2 n == N & 2 n ,如果相等,表示为 1
我们通过一个场景来模拟试验上面的 3 个操作。首先,我们定义三个变量或常量:
private static final int HANDSOME_MASK = 0x1; //帅哥掩码,0001
private static final int FLESH_MEAT_MASK = HANDSOME_MASK << 1; //小鲜肉掩码,向左移动 1位,二进制就是 0010
private int me = 0;//初始化为 0,即不是小鲜肉,也不是帅哥
继承自ViewGroup的侧边栏
提供侧边栏的两种实现方案。其一是继承自 ViewGroup,其二是继承自HorizontalScrollView。显然,第一种实现会更加复杂,第二种要相对简单些。显然这两种都是应该掌握的。
不管是哪一种方法,侧边栏的结构都是由两部分构成:侧边栏和主界面
<declare-styleable name="SliderMenu"> <!-- 侧边栏宽度 --> <attr name="sliding_width" format="dimension"/> <!-- 分割线宽度 --> <attr name="separator" format="dimension"/> <!-- 手指滑动感应宽度 --> <attr name="touch_width" format="dimension"/> </declare-styleable>
public class SliderMenu extends ViewGroup {
private static final String TAG = "SliderMenu"; private static final int DO_MOVING = 0x001;//可以滑动 private static final int NOT_MOVING = 0x002;//不可以滑动 private int moving = NOT_MOVING; //是否可以滑动,默认不能滑动 private static final int FLAG_SEPARATOR = 0x1;//标记变量,是否有分割线,占用最后一位 private static final int FLAG_IS_OPEN = FLAG_SEPARATOR << 1;//标记变量,是否已打开,占用倒数第二位 private int flags = FLAG_SEPARATOR >> 1;//存储标记变量 private int slidingWidth;//侧边栏宽度 private float separator;//分割线宽度 private int touchWidth;//感应宽度 private int screenWidth;//屏幕宽度 private Paint paint; private int preX, firstX; private Scroller scroller; public SliderMenu(Context context) { this(context, null, 0); } public SliderMenu(Context context, AttributeSet attrs) { this(context, attrs, 0); } / * 初始化 * @param context * @param attrs * @param defStyleAttr */ public SliderMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.SliderMenu); slidingWidth = a.getDimensionPixelSize( R.styleable.SliderMenu_sliding_width, (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 150, getResources().getDisplayMetrics())); separator = a.getDimensionPixelSize(R.styleable.SliderMenu_separator, (int)TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 1 , getResources().getDisplayMetrics())); touchWidth = a.getDimensionPixelSize( R.styleable.SliderMenu_touch_width, (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 50, getResources().getDisplayMetrics())); if(separator > 0) flags = flags | FLAG_SEPARATOR; a.recycle(); screenWidth = getScreenWidth(context); setBackgroundColor(Color.alpha(255)); paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(Color.GRAY); paint.setStrokeWidth(separator); scroller = new Scroller(context); } / * 子元素不能超过 2 个 * @param child * @param index * @param params */ @Override public void addView(View child, int index, LayoutParams params) { super.addView(child, index, params); if(getChildCount() > 2){ throw new ArrayIndexOutOfBoundsException("Children count can't be more than 2."); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { System.out.println("SlideMenu的位置left:" + left + " top:" + top + " right:" + right + " bottom:" + bottom);// left:0 top:0 right:540 bottom:863 //SlideMenu的位置就是上面的System.out.println输出的log //这里需要确定的是SlideMenu的子View的位置 //侧边栏子View初始位置是隐藏在手机屏幕的左边的,注意,此时menuView经过measure之后,menuView.getMeasuredWidth()就等于了menuWidth menuView.layout(-menuWidth, 0, 0, menuView.getMeasuredHeight()); //主布局子View的位置,初始是显示在整个屏幕中 mainView.layout(0, 0, mainView.getMeasuredWidth(), mainView.getMeasuredHeight()); } private View menuView;//侧边栏子View private View mainView;//主体子View private int menuWidth;//侧边栏的宽度 @Override protected void onFinishInflate() { super.onFinishInflate(); menuView = getChildAt(0); //侧边栏布局 mainView = getChildAt(1); //主体布局 menuWidth = menuView.getLayoutParams().width;//侧边栏的宽度 } / * 获取屏幕宽度 * @param context * @return */ private int getScreenWidth(Context context){ WindowManager wm = (WindowManager) getContext() .getSystemService(Context.WINDOW_SERVICE); Point point = new Point(); wm.getDefaultDisplay().getSize(point); return point.x; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { measureChildren(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); } / * 总宽度 = 侧边栏宽度 + 主界面宽度 + 分割线宽度 * @param widthMeasureSpec * @return */ private int measureWidth(int widthMeasureSpec){ int mode = MeasureSpec.getMode(widthMeasureSpec); int size = MeasureSpec.getSize(widthMeasureSpec); if(mode == MeasureSpec.AT_MOST){
throw new IllegalStateException("layout_width can not be wrap_content."); } return (int) (screenWidth + slidingWidth + separator); } private int measureHeight(int heightMeasureSpec){ int mode = MeasureSpec.getMode(heightMeasureSpec); int size = MeasureSpec.getSize(heightMeasureSpec); if(mode == MeasureSpec.AT_MOST){ throw new IllegalStateException("layout_width can not be wrap_content"); } int height = 0; if(mode == MeasureSpec.EXACTLY){ height = size; } return height; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //画分割线 if((flags & FLAG_SEPARATOR) == FLAG_SEPARATOR){ int left = (int) (slidingWidth + separator / 2); canvas.drawLine(left, 0, left, getMeasuredHeight(), paint); } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: Log.i(TAG, "X:" + ev.getX()); if((flags & FLAG_IS_OPEN) == FLAG_IS_OPEN){ moving = DO_MOVING; }else{ if(ev.getX() > touchWidth) moving = NOT_MOVING; else moving = DO_MOVING; } break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: moving= NOT_MOVING; break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { if(moving == NOT_MOVING) return false; int x = (int) event.getX(); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: preX = x; firstX = x; break;case MotionEvent.ACTION_MOVE: int dx = x - preX; this.scrollBy(-dx, 0); preX = x; break; case MotionEvent.ACTION_UP: dx = x - firstX; Log.i(TAG, "dx: " + dx); int remain = slidingWidth - Math.abs(dx); //dx 右为正,左为负 boolean isOpen = (flags & FLAG_IS_OPEN) == FLAG_IS_OPEN; if(dx > 0 && !isOpen) { scroller.startScroll(getScrollX(), 0, -remain, 0); flags = flags | FLAG_IS_OPEN; }else if (dx < 0 && isOpen){ scroller.startScroll(getScrollX(), 0, remain, 0); flags = flags & ~FLAG_IS_OPEN; }else{ //校正(比如向右滑又向左滑) scroller.startScroll(getScrollX(), 0, dx, 0); } invalidate(); break; } return moving == DO_MOVING; } @Override public void computeScroll() { if(scroller.computeScrollOffset()){ this.scrollTo(scroller.getCurrX(), 0); postInvalidate(); } } / * 打开侧边栏 */ public void open(){ boolean isOpen = (flags & FLAG_IS_OPEN) == FLAG_IS_OPEN; if(!isOpen){ scroller.startScroll(slidingWidth, 0, -slidingWidth, 0); invalidate(); flags = flags | FLAG_IS_OPEN; } } / * 关闭侧边栏 */ public void close(){ boolean isOpen = (flags & FLAG_IS_OPEN) == FLAG_IS_OPEN; if(isOpen){ scroller.startScroll(0, 0, slidingWidth, 0); invalidate(); flags = flags & ~FLAG_IS_OPEN; } } / * 打开/关闭侧边栏 */ public void toggle(){ boolean isOpen = (flags & FLAG_IS_OPEN) == FLAG_IS_OPEN; if(isOpen) close(); else open(); } }
上面代码中使用了位运算来保存两个标识数据:打开状态和是否需要绘制分割线。
<?xml version="1.0" encoding="utf-8"?> <com.trkj.lizanhong.chapter9.SliderMenu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:trkj="http://schemas.android.com/apk/res-auto" android:id="@+id/sm" android:layout_width="match_parent" android:layout_height="match_parent" trkj:separator="0.5dp" trkj:sliding_width="200dp" trkj:touch_width="50dp"> <!-- 侧边栏--> <LinearLayout android:id="@+id/sliding_menu" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="test" android:text="测试" /> </LinearLayout> <!-- 主界面 --> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_green_light"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="toggle" android:text="显示" /> </FrameLayout> </com.trkj.lizanhong.chapter9.SliderMenu>
定义继承自 Activity 名为 MainActivity 类,该类中响应 FrameLayout 布局中的按钮的单击事件,用于显示或隐藏侧边栏。内容如下:
public class MainActivity extends ActionBarActivity {
private SliderMenu sm; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.sliding_menu); sm = (SliderMenu) findViewById(R.id.sm); this.setTitle("侧边栏"); } public void test(View view){ Toast.makeText(this, "test", Toast.LENGTH_LONG).sho w(); } public void toggle(View view){ sm.toggle(); } }
继承自HorizontalScrollView 的侧边栏
这次实现的侧边栏在功能上有了少许变化:
Ø 在任何地方滑动都可以打开侧边栏;
Ø 滑动距离如果不超过侧边栏一半,则回退;
Ø 不再支持分割线。
HorizontalScrollView 作为水平滚动视图,已经实现了平滑滚动的功能,这可以让我们不再通过 Scroller 实现自动滑动。HorizontalScrollView 定义的平滑滚动方法原型为:
另外,滚动视图有一个规则,子组件只能有一个,所以,我们必须将侧边栏和主界面使用一个水平的 LinarLayout 包裹起来,因此,布局文件的结构与上一个侧边栏也会有所不同。
侧边栏支持指定宽度,在 attrs.xml 中添加如下的配置:
<declare-styleable name="SlidingMenu"> <!-- 指定侧边栏的宽度 --> <attr name="left_padding_width" format="dimension"/> </declare-styleable>
/ * 指定侧边栏和主界面的宽 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { once = true; //水平滚动 View 只能有一个直接子组件 LinearLayout subView = (LinearLayout) this.getChildAt(0); //指定侧边栏和主界面的宽 //第 0 个子组件就是侧边栏,第 1 个组件就是主界面 LinearLayout slidingMenu = (LinearLayout) subView.getChildAt(0); //第 1 个子组件是主界面 ViewGroup content = (ViewGroup) subView.getChildAt(1); //设置侧边的宽度 slidingMenu.getLayoutParams().width = leftPaddingWidth; //设置主界面的宽度 content.getLayoutParams().width = this.getScreenWidth(); super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
而在 onLayout()方法中,我们只需要在运行时将侧边栏隐藏起来。
/ * 初始状态下在该方法中隐藏侧边栏 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); //下面的代码只调用一次 if(once){ //隐藏侧边栏 this.scrollTo(leftPaddingWidth, 0); } once = false; }
HorizontalScrollView 类中早已实现了子组件的平滑滚动,我们要做的就是当手指松开后的惯性滚动。当手指滑动距离超过侧边栏的一半则继续向前滑动,否则回滚到初始状态。
@Override public boolean onTouchEvent(MotionEvent ev) { //当手指松开的时候,决定侧边栏是显示还是隐藏 //如果侧边栏滚出屏幕的宽度大于侧边栏的一半,则隐藏侧边栏 //如果侧边栏滚出屏幕的宽度小于等于侧边栏的一半,则显示侧边栏 if(ev.getAction() == MotionEvent.ACTION_UP){ int dx = this.getScrollX(); int halfWidth = this.leftPaddingWidth / 2; Log.i("SlidingMenu", "dx:" + this.getScrollX() + " halfWidth:" + halfWidth); if(dx < halfWidth){ //显示侧边栏 this.smoothScrollTo(0, 0); Log.i("SlidingMenu", "显示"); this.isOpen = true; }else{ //隐藏侧边栏 this.smoothScrollTo(leftPaddingWidth, 0); Log.i("SlidingMenu", "隐藏"); this.isOpen = false; } return true; } return super.onTouchEvent(ev); }
SlidingMenu 的全部源码如下(省略了前面已列出的代码):
public class SlidingMenu extends HorizontalScrollView {
private int leftPaddingWidth; //侧边栏的宽度 private boolean isOpen = false;//侧边栏是否打开 private boolean once = false;//默认只隐藏一次 / * 获取屏幕宽度 * @return */ private int getScreenWidth(){ //请参考上一案例 } / * 指定侧边栏和主界面的宽 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { …… } / * 默认情况下在该方法中隐藏侧边栏 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { …… } @Override public boolean onTouchEvent(MotionEvent ev) { …… } / * 打开 */ public void open(){ if(!isOpen){ this.smoothScrollTo(0, 0); isOpen = true; } } / * 隐藏 */ public void hide(){ if(isOpen){ this.smoothScrollTo(leftPaddingWidth, 0); isOpen = false; } } / * 打开隐藏 */ public void toggle(){ if(isOpen){ hide(); }else{ open(); } } public SlidingMenu(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public SlidingMenu(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingMenu); leftPaddingWidth = a.getDimensionPixelSize( R.styleable.SlidingMenu_left_padding_width, (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 200, context.getResources().getDisplayMetrics())); a.recycle(); } public SlidingMenu(Context context) { super(context); } }
定义 sliding_menu2.xml 布局文件,仔细比较该布局文件与上一解决方案中布局文件的区别。因为实现不同,所以二者可能会有一些不得已的限制。从 ViewGroup 上继承也许是灵活性最好的。
<?xml version="1.0" encoding="utf-8"?> <com.trkj.lizanhong.chapter9.SlidingMenu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:trkj="http://schemas.android.com/apk/res-auto" android:id="@+id/slidingMenu" android:layout_width="wrap_content" android:layout_height="match_parent" android:scrollbars="none" trkj:left_padding_width="200dp"> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="horizontal"> <!-- 侧边栏 --> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:background="#FF0000" android:orientation="vertical"></LinearLayout> <!-- 主界面 --> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:background="#FFFFFF" android:orientation="vertical"> <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="toggle" android:text="侧边栏" /> </LinearLayout> </LinearLayout> </com.trkj.lizanhong.chapter9.SlidingMenu>
运行效果如图所示。
谢谢认真观读本文的每一位小伙伴,衷心欢迎小伙伴给我指出文中的错误,也欢迎小伙伴与我交流学习。
欢迎爱学习的小伙伴加群一起进步:
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/147873.html