轻量级--层叠的侧边栏
来源:互联网 发布:木马编程 编辑:程序博客网 时间:2024/05/19 19:33
github上的自定义view:
package com.mstarc.app.mstarchelper2.functions.home.widget;/** * Created by Administrator on 2017/4/13. */import android.content.Context;import android.graphics.Bitmap;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.PorterDuff.Mode;import android.graphics.Rect;import android.graphics.Region.Op;import android.os.Build;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.view.animation.Animation;import android.view.animation.DecelerateInterpolator;import android.view.animation.Transformation;import android.widget.FrameLayout;import java.util.LinkedList;import java.util.Queue;/** * 轻量级侧边栏:分为菜单栏和内容栏。 * 仅支持从左边或右边开启侧边栏菜单,不支持同时,且滑开菜单栏的动画不能修改; * 实现方法:滑动的是内容栏,菜单栏不动。这个是使用Canvas.translate()配合动画来完成内容栏滑动效果的。 * 不过我们自己写的话,大多数会采用scrollTo()函数来完成。 */public class SlideHolder extends FrameLayout { public final static int DIRECTION_LEFT = 1; // 左侧边栏 public final static int DIRECTION_RIGHT = -1; // 右侧边栏 protected final static int MODE_READY = 0; // 标记菜单栏还没有打开,可以滑动侧边栏 protected final static int MODE_SLIDE = 1; // 标记菜单栏正在划开侧边栏过程中... protected final static int MODE_FINISHED = 2; // 标记菜单栏是否已经打开 private Bitmap mCachedBitmap; // 与内容栏的宽高相等的图片 private Canvas mCachedCanvas; // 画布,用于 private Paint mCachedPaint; private View mMenuView; // 菜单栏 private int mMode = MODE_READY; private int mDirection = DIRECTION_LEFT; // 从左边打开侧边栏或右边 private int mOffset = 0; // 内容栏的当前偏移量 private int mStartOffset; // 内容栏开始移动的位置的偏移量 private int mEndOffset; // 内容栏移动结束的位置的偏移量 private boolean mEnabled = true; private boolean mInterceptTouch = true; // 标记是否允许侧边栏截获触摸手势 private boolean mAlwaysOpened = false; // 标记侧边栏是否是持续打开的,用于大屏幕的Pad private boolean mDispatchWhenOpened = false; // 标记当侧边栏打开时,是否分发触摸手势 private Queue<Runnable> mWhenReady = new LinkedList<Runnable>(); // 打开/关闭菜单栏的线程对象集合 private OnSlideListener mListener; // 滑动菜单栏结束的回调 public SlideHolder(Context context) { super(context); initView(); } public SlideHolder(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public SlideHolder(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(); } private void initView() { mCachedPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); } @Override public void setEnabled(boolean enabled) { mEnabled = enabled; } @Override public boolean isEnabled() { return mEnabled; } /** * 设置侧边栏打开方向 * * @param direction * - direction in which SlideHolder opens. Can be: * DIRECTION_LEFT, DIRECTION_RIGHT */ public void setDirection(int direction) { closeImmediately(); mDirection = direction; } /** * 设置允许截获触摸手势事件 * * @param allow * - if false, SlideHolder won't react to swiping gestures (but * still will be able to work by manually invoking mathods) */ public void setAllowInterceptTouch(boolean allow) { mInterceptTouch = allow; } /** * 判断是否允许侧边栏截获触摸手势事件 * @return */ public boolean isAllowedInterceptTouch() { return mInterceptTouch; } /** * 判断是否允许侧边栏分发触摸手势事件 * @param dispatch * - if true, in open state SlideHolder will dispatch touch * events to main layout (in other words - it will be clickable) */ public void setDispatchTouchWhenOpened(boolean dispatch) { mDispatchWhenOpened = dispatch; } public boolean isDispatchTouchWhenOpened() { return mDispatchWhenOpened; } /** * 设置侧边栏总是打开,用于大屏幕的Pad设备 * @param opened * - if true, SlideHolder will always be in opened state (which * means that swiping won't work) */ public void setAlwaysOpened(boolean opened) { mAlwaysOpened = opened; requestLayout(); } /** * 获取侧边栏菜单的偏移量 * @return */ public int getMenuOffset() { return mOffset; } public void setOnSlideListener(OnSlideListener lis) { mListener = lis; } /** * 菜单栏是否已经打开 * @return */ public boolean isOpened() { return mAlwaysOpened || mMode == MODE_FINISHED; } /** * 菜单栏的开关 * @param immediately */ public void toggle(boolean immediately) { if (immediately) { toggleImmediately(); } else { toggle(); } } /** * 菜单栏开关 */ public void toggle() { if (isOpened()) { close(); } else { open(); } } /** * 菜单栏立即开关 */ public void toggleImmediately() { if (isOpened()) { closeImmediately(); } else { openImmediately(); } } /** * 开启菜单栏 * @return */ public boolean open() { if (isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) { return false; } if (!isReadyForSlide()) { mWhenReady.add(new Runnable() { @Override public void run() { open(); } }); return true; } initSlideMode(); Animation anim = new SlideAnimation(mOffset, mEndOffset); anim.setAnimationListener(mOpenListener); startAnimation(anim); invalidate(); return true; } /** * 立即打开侧边栏菜单 * @return */ public boolean openImmediately() { if (isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) { return false; } if (!isReadyForSlide()) { mWhenReady.add(new Runnable() { @Override public void run() { openImmediately(); } }); return true; } mMenuView.setVisibility(View.VISIBLE); mMode = MODE_FINISHED; requestLayout(); if (mListener != null) { mListener.onSlideCompleted(true); } return true; } /** * 关闭菜单栏 * @return */ public boolean close() { if (!isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) { return false; } if (!isReadyForSlide()) { mWhenReady.add(new Runnable() { @Override public void run() { close(); } }); return true; } initSlideMode(); Animation anim = new SlideAnimation(mOffset, mEndOffset); // 关闭菜单栏的动画 anim.setAnimationListener(mCloseListener); startAnimation(anim); invalidate(); return true; } /** * 快速关闭菜单栏 * @return */ public boolean closeImmediately() { if (!isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) { return false; } if (!isReadyForSlide()) { // 侧边栏还没有准备好滑动,则加入队列中等待执行关闭菜单栏 mWhenReady.add(new Runnable() { @Override public void run() { closeImmediately(); } }); return true; } mMenuView.setVisibility(View.GONE); // 直接把菜单栏设为GONE(够快速了吧!!) mMode = MODE_READY; requestLayout(); if (mListener != null) { mListener.onSlideCompleted(false); } return true; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int parentLeft = 0; final int parentTop = 0; final int parentRight = r - l; final int parentBottom = b - t; View menu = getChildAt(0); int menuWidth = menu.getMeasuredWidth(); if (mDirection == DIRECTION_LEFT) { menu.layout(parentLeft, parentTop, parentLeft + menuWidth, parentBottom); } else { menu.layout(parentRight - menuWidth, parentTop, parentRight, parentBottom); } if (mAlwaysOpened) { if (mDirection == DIRECTION_LEFT) { mOffset = menuWidth; } else { mOffset = 0; } } else if (mMode == MODE_FINISHED) { mOffset = mDirection * menuWidth; } else if (mMode == MODE_READY) { mOffset = 0; } View main = getChildAt(1); main.layout(parentLeft + mOffset, parentTop, parentLeft + mOffset + main.getMeasuredWidth(), parentBottom); invalidate(); Runnable rn; while ((rn = mWhenReady.poll()) != null) { rn.run(); } } /** * 是否准备好打开菜单栏或关闭菜单栏,即是否可以滑动?? * @return 返回false的唯一情况是:当前正在滑动菜单栏中... */ private boolean isReadyForSlide() { return (getWidth() > 0 && getHeight() > 0); } @Override protected void onMeasure(int wSp, int hSp) { mMenuView = getChildAt(0); if (mAlwaysOpened) { View main = getChildAt(1); if (mMenuView != null && main != null) { measureChild(mMenuView, wSp, hSp); LayoutParams lp = (LayoutParams) main.getLayoutParams(); if (mDirection == DIRECTION_LEFT) { lp.leftMargin = mMenuView.getMeasuredWidth(); } else { lp.rightMargin = mMenuView.getMeasuredWidth(); } } } super.onMeasure(wSp, hSp); } private byte mFrame = 0; @Override protected void dispatchDraw(Canvas canvas) { try { if (mMode == MODE_SLIDE) { View main = getChildAt(1); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { /* * On new versions we redrawing main layout only if it's * marked as dirty */ if (main.isDirty()) { mCachedCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR); main.draw(mCachedCanvas); } } else { /* * On older versions we just redrawing our cache every 5th * frame */ if (++mFrame % 5 == 0) { mCachedCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR); main.draw(mCachedCanvas); } } /* * Draw only visible part of menu */ View menu = getChildAt(0); final int scrollX = menu.getScrollX(); final int scrollY = menu.getScrollY(); canvas.save(); if (mDirection == DIRECTION_LEFT) { canvas.clipRect(0, 0, mOffset, menu.getHeight(), Op.REPLACE); } else { int menuWidth = menu.getWidth(); int menuLeft = menu.getLeft(); canvas.clipRect(menuLeft + menuWidth + mOffset, 0, menuLeft + menuWidth, menu.getHeight()); } canvas.translate(menu.getLeft(), menu.getTop()); canvas.translate(-scrollX, -scrollY); menu.draw(canvas); canvas.restore(); canvas.drawBitmap(mCachedBitmap, mOffset, 0, mCachedPaint); } else { if (!mAlwaysOpened && mMode == MODE_READY) { mMenuView.setVisibility(View.GONE); } super.dispatchDraw(canvas); } } catch (IndexOutOfBoundsException e) { /* * Possibility of crashes on some devices (especially on Samsung). * Usually, when ListView is empty. */ } } private int mHistoricalX = 0; private boolean mCloseOnRelease = false; /** * 分发触摸手势事件,这个是触摸手势被识别进入的第一个函数 */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (((!mEnabled || !mInterceptTouch) && mMode == MODE_READY) || mAlwaysOpened) { return super.dispatchTouchEvent(ev); } if (mMode != MODE_FINISHED) { onTouchEvent(ev); if (mMode != MODE_SLIDE) { super.dispatchTouchEvent(ev); } else { MotionEvent cancelEvent = MotionEvent.obtain(ev); cancelEvent.setAction(MotionEvent.ACTION_CANCEL); super.dispatchTouchEvent(cancelEvent); cancelEvent.recycle(); } return true; } else { final int action = ev.getAction(); Rect rect = new Rect(); View menu = getChildAt(0); menu.getHitRect(rect); if (!rect.contains((int) ev.getX(), (int) ev.getY())) { if (action == MotionEvent.ACTION_UP && mCloseOnRelease && !mDispatchWhenOpened) { close(); mCloseOnRelease = false; } else { if (action == MotionEvent.ACTION_DOWN && !mDispatchWhenOpened) { mCloseOnRelease = true; } onTouchEvent(ev); } if (mDispatchWhenOpened) { super.dispatchTouchEvent(ev); } return true; } else { onTouchEvent(ev); ev.offsetLocation(-menu.getLeft(), -menu.getTop()); menu.dispatchTouchEvent(ev); return true; } } } /** * 处理触摸手势事件 * @param ev * @return */ private boolean handleTouchEvent(MotionEvent ev) { if (!mEnabled) { return false; } float x = ev.getX(); if (ev.getAction() == MotionEvent.ACTION_DOWN) { mHistoricalX = (int) x; return true; } if (ev.getAction() == MotionEvent.ACTION_MOVE) { float diff = x - mHistoricalX; // 判断是可以认为是滑动手势的 if ((mDirection * diff > 50 && mMode == MODE_READY) || (mDirection * diff < -50 && mMode == MODE_FINISHED)) { mHistoricalX = (int) x; initSlideMode(); } else if (mMode == MODE_SLIDE) { // 正处于滑动过程中... mOffset += diff; mHistoricalX = (int) x; if (!isSlideAllowed()) { finishSlide(); } } else { return false; } } if (ev.getAction() == MotionEvent.ACTION_UP) { if (mMode == MODE_SLIDE) { finishSlide(); } mCloseOnRelease = false; return false; } return mMode == MODE_SLIDE; } @Override public boolean onTouchEvent(MotionEvent ev) { boolean handled = handleTouchEvent(ev); invalidate(); return handled; } /* * 初始化侧边栏菜单的模式:打开和关闭菜单栏的时候都需要重新设置 侧边栏参数, * 这个函数在配合onLayout()函数,就实现了侧边栏动画。 */ private void initSlideMode() { mCloseOnRelease = false; View v = getChildAt(1); // 获取内容栏 if (mMode == MODE_READY) { // 侧边栏菜单未打开时 mStartOffset = 0; mEndOffset = mDirection * getChildAt(0).getWidth(); } else { // 侧边栏菜单界面已经打开后 mStartOffset = mDirection * getChildAt(0).getWidth(); mEndOffset = 0; // 内容界面最后位置的偏移量 } mOffset = mStartOffset; // 设置当前内容栏的偏移量 if (mCachedBitmap == null || mCachedBitmap.isRecycled() || mCachedBitmap.getWidth() != v.getWidth()) { mCachedBitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888); mCachedCanvas = new Canvas(mCachedBitmap); } else { mCachedCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR); } v.setVisibility(View.VISIBLE); mCachedCanvas.translate(-v.getScrollX(), -v.getScrollY()); v.draw(mCachedCanvas); mMode = MODE_SLIDE; mMenuView.setVisibility(View.VISIBLE); } /* * 是否允许滑动 */ private boolean isSlideAllowed() { return (mDirection * mEndOffset > 0 && mDirection * mOffset < mDirection * mEndOffset && mDirection * mOffset >= mDirection * mStartOffset) || (mEndOffset == 0 && mDirection * mOffset > mDirection * mEndOffset && mDirection * mOffset <= mDirection * mStartOffset); } /* * 打开完毕的回调 */ private void completeOpening() { mOffset = mDirection * mMenuView.getWidth(); // 设置当前内容栏的偏移量 requestLayout(); post(new Runnable() { @Override public void run() { mMode = MODE_FINISHED; mMenuView.setVisibility(View.VISIBLE); } }); if (mListener != null) { mListener.onSlideCompleted(true); } } /** * 以动画方式打开菜单栏的回调函数 */ private Animation.AnimationListener mOpenListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { completeOpening(); } }; /** * 完成关闭菜单栏的回调 */ private void completeClosing() { mOffset = 0; requestLayout(); post(new Runnable() { @Override public void run() { mMode = MODE_READY; mMenuView.setVisibility(View.GONE); } }); if (mListener != null) { mListener.onSlideCompleted(false); } } /** * 关闭菜单栏的动画回调 */ private Animation.AnimationListener mCloseListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { completeClosing(); } }; /* * 结束滑动 */ private void finishSlide() { if (mDirection * mEndOffset > 0) { // 菜单栏在左侧 // 如果当前滑动距离大于结束距离的一般,则认为是滑动操作 if (mDirection * mOffset > mDirection * mEndOffset / 2) { if (mDirection * mOffset > mDirection * mEndOffset) { // 防止滑过界 mOffset = mEndOffset; } // 打开菜单栏动画 Animation anim = new SlideAnimation(mOffset, mEndOffset); anim.setAnimationListener(mOpenListener); startAnimation(anim); } else { if (mDirection * mOffset < mDirection * mStartOffset) { // 防止滑过界 mOffset = mStartOffset; } // 关闭菜单栏动画 Animation anim = new SlideAnimation(mOffset, mStartOffset); anim.setAnimationListener(mCloseListener); startAnimation(anim); } } else { // 菜单栏在右侧 if (mDirection * mOffset < mDirection * mStartOffset / 2) { if (mDirection * mOffset < mDirection * mEndOffset) { mOffset = mEndOffset; } Animation anim = new SlideAnimation(mOffset, mEndOffset); anim.setAnimationListener(mCloseListener); startAnimation(anim); } else { if (mDirection * mOffset > mDirection * mStartOffset) { mOffset = mStartOffset; } Animation anim = new SlideAnimation(mOffset, mStartOffset); anim.setAnimationListener(mOpenListener); startAnimation(anim); } } } /* * 偏移动画 */ private class SlideAnimation extends Animation { private static final float SPEED = 0.6f; private float mStart; private float mEnd; public SlideAnimation(float fromX, float toX) { mStart = fromX; mEnd = toX; setInterpolator(new DecelerateInterpolator()); float duration = Math.abs(mEnd - mStart) / SPEED; setDuration((long) duration); } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { super.applyTransformation(interpolatedTime, t); float offset = (mEnd - mStart) * interpolatedTime + mStart; mOffset = (int) offset; postInvalidate(); } } public static interface OnSlideListener { public void onSlideCompleted(boolean opened); }}
activity布局:
<?xml version="1.0" encoding="utf-8"?><com.mstarc.app.mstarchelper2.functions.home.widget.SlideHolder xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/slideHolder" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.mstarc.app.mstarchelper2.functions.home.ui.HomeActivity"> <ScrollView android:layout_width="200dp" android:layout_height="match_parent"> <include layout="@layout/home_side"></include> </ScrollView> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/home_top_bar_bg" /> <LinearLayout android:id="@+id/ly_param_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <com.mstarc.app.mstarchelper2.common.base.TopTitleLayout android:id="@+id/ttl_home_title" android:layout_width="match_parent" android:layout_height="wrap_content"/> <LinearLayout android:layout_width="match_parent" android:layout_height="126px" android:layout_alignParentTop="true" android:background="@color/trans" android:orientation="horizontal" > <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:orientation="vertical"> <TextView android:id="@+id/tv_home_battery_left" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="10%" android:textColor="@color/white" android:textSize="@dimen/home_top_params_size"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="电量" android:textColor="@color/white" android:textSize="@dimen/home_top_label_size"/> </LinearLayout> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:orientation="vertical"> <ImageView android:id="@+id/iv_home_blue_connect" android:layout_width="wrap_content" android:layout_height="70px" android:src="@drawable/home_bar_icon_connected"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="3px" android:text="已连接" android:textColor="@color/white" android:textSize="@dimen/home_top_label_size"/> </LinearLayout> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:orientation="vertical"> <TextView android:id="@+id/tv_home_step" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="100000" android:textColor="@color/white" android:textSize="@dimen/home_top_params_size"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="步数" android:textColor="@color/white" android:textSize="@dimen/home_top_label_size"/> </LinearLayout> </LinearLayout> </LinearLayout> <com.zhy.autolayout.AutoLinearLayout android:id="@+id/test" android:layout_width="match_parent" android:layout_height="@dimen/home_lunbo_height" android:layout_below="@+id/ly_param_bar" android:background="@color/black" android:orientation="vertical"> </com.zhy.autolayout.AutoLinearLayout> <GridView android:id="@+id/gv_fun" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/test" android:numColumns="3" /> </RelativeLayout></com.mstarc.app.mstarchelper2.functions.home.widget.SlideHolder>
activity:SlideHolder mSlideHolder;mSlideHolder = (SlideHolder) findViewById(R.id.slideHolder);mSlideHolder.toggle();
0 0
- 轻量级--层叠的侧边栏
- 一个方便使用的轻量级侧边栏
- 开源项目:AndroidSideMenu(轻量级侧边栏)
- 简单的侧边栏
- 侧边栏的实现
- 侧边栏的实现----网易新闻的侧边栏效果
- 侧边栏SlidingMenu的应用
- 侧边栏交互的利弊
- 带侧边栏的Fragment
- 类似于qq的侧边栏
- 上下滑动的侧边栏
- SlidingMenu侧边栏的实现
- 侧边栏
- 侧边栏
- 侧边栏
- 简单的折叠侧边栏的制作
- 基于CCoolBar侧边栏的实现
- WordPress侧边栏漂浮的实现
- [leetCode刷题笔记]525. Contiguous Array
- find 命令
- PS“无法完成请求,因为智能对象不能直接进行编辑”解决办法
- window DOS命令共享目录
- Filesystem Case-Sensitivity Mismatch
- 轻量级--层叠的侧边栏
- 初识smali
- Html和struct2中select默认值
- GreenDao 3.0使用
- Maven手动依赖jar包到本地仓库
- LiteOS学习第五篇——任务切换
- 使用php的swoole扩展实现数据实时更新(上)
- EndNote的基础学习
- linux exercise 13