Android自定义View(六)——打造更完美的侧滑
2015-12-19 17:35
543 查看
开篇之前,先感谢@鸿洋_,我自定义view的大部分知识都是源于他的博客。
对代码有疑问的请移步:http://blog.csdn.net/lmj623565791/article/details/39257409,但此文是里面有些叙述是错误的,请大家自行测试。
以下分析完全是基于ScrollView,和View本身的一些方法是有出入的
虽然已经有很多人写过侧滑菜单了,但是我还是要写。为什么呢?因为我在测试他们写的代码时,发现了一些概念性的错误,虽然他们的效果是出来了(有的也没出来),但是其实他们得到的这个效果具有偶然性。
就拿ScrollView的scrollTo(float x,float y)方法和View的getScrollX()方法来说,至少在我测试下来有些概念是不正确的。getScrollX是view的方法,对应的是view的mScrollX变量,返回的是当前scroll在View的坐标系中的位置,最小也就是0,最大也就是(ScrollView的直接子布局的宽度-ScrollView的宽度)的绝对值(参见ScrollView源码的clamp方法),不可能像有人说的会出现负值,可以形象的理解为ScrollBar在ScrollView里的x坐标;scrollTo中的参数,也就是去改变ScrollBar在ScrollView里的位置的:
调用scrollTo:
——>调用clamp方法:
——>如果有必要滑动,调用View的scrollTo方法
——>最后再看getScrollX
基于以上的一些见解,所以有了优化过后的侧滑:
一些自定义属性(不要吐槽我的命名(^o^)/)
最后,无图无真相啊:
对代码有疑问的请移步:http://blog.csdn.net/lmj623565791/article/details/39257409,但此文是里面有些叙述是错误的,请大家自行测试。
以下分析完全是基于ScrollView,和View本身的一些方法是有出入的
虽然已经有很多人写过侧滑菜单了,但是我还是要写。为什么呢?因为我在测试他们写的代码时,发现了一些概念性的错误,虽然他们的效果是出来了(有的也没出来),但是其实他们得到的这个效果具有偶然性。
就拿ScrollView的scrollTo(float x,float y)方法和View的getScrollX()方法来说,至少在我测试下来有些概念是不正确的。getScrollX是view的方法,对应的是view的mScrollX变量,返回的是当前scroll在View的坐标系中的位置,最小也就是0,最大也就是(ScrollView的直接子布局的宽度-ScrollView的宽度)的绝对值(参见ScrollView源码的clamp方法),不可能像有人说的会出现负值,可以形象的理解为ScrollBar在ScrollView里的x坐标;scrollTo中的参数,也就是去改变ScrollBar在ScrollView里的位置的:
调用scrollTo:
/** * {@inheritDoc} * * <p>This version also clamps the scrolling to the bounds of our child. */ @Override public void scrollTo(int x, int y) { // we rely on the fact the View.scrollBy calls scrollTo. if (getChildCount() > 0) { View child = getChildAt(0); x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth()); y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight()); if (x != mScrollX || y != mScrollY) { super.scrollTo(x, y); } } }
——>调用clamp方法:
/* *1.如果传入的参数小于0或者child的范围还在ScrollView范围内或相等,会直接返回0; *2.如果ScrollView的范围加上要调整的范围大于了child的范围,那么其实只需移动到child的最后端即可。 *所以当ScrollView的scrollTo调用 super.scrollTo()时,传入的参数永远不会小于0。 */ private static int clamp(int n, int my, int child) { if (my >= child || n < 0) { return 0; } if ((my + n) > child) { return child - my; } return n; }
——>如果有必要滑动,调用View的scrollTo方法
/** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */ public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { //这里才发生了对mScrollX和mScrollY的赋值 int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; //重绘和回调的处理 invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } }
——>最后再看getScrollX
/** * Return the scrolled left position of this view. This is the left edge of * the displayed part of your view. You do not need to draw any pixels * farther left, since those are outside of the frame of your view on * screen. * * @return The left edge of the displayed part of your view, in pixels. */ public final int getScrollX() { return mScrollX; }
基于以上的一些见解,所以有了优化过后的侧滑:
/** * 包名:com.ykbjson.customview * 描述:基于HorizontalScrollView的滑动菜单 * 创建者:yankebin * 日期:2015/12/15 */ public class SlideMenu extends HorizontalScrollView implements SlideBase { private String TAG = getClass().getSimpleName(); /** * 滑动偏移量 */ private static final int BASE_SLIDE_BLOCK = 12; /** * 屏幕像素密度 */ private float density; /** * 菜单效果 */ private int mode; /** * 是否已加载过一次layout,这里onLayout中的初始化只需加载一次 */ private boolean loadOnce; /** * 左侧布局对象。 */ private View leftLayout; /** * 右侧布局对象。 */ private View rightLayout; /** * 菜单宽度 */ private int menuWidth; /** * 滑动开始时的x坐标 */ private float downX; /** * 滑动开始时的y坐标 */ private float downY; /** * 菜单状态 */ private boolean isMenuOpen; private boolean slideMenuContent; private float menuAlpha; private float contentAlpha; private float menuScale; private float contentScal; private float menuMove; /** * 重写SlidingLayout的构造函数 * * @param context */ public SlideMenu(Context context) { this(context, null); } /** * 重写SlidingLayout的构造函数 * * @param context * @param attrs */ public SlideMenu(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * 重写SlidingLayout的构造函数 * * @param context * @param attrs * @param defaultStyle */ public SlideMenu(Context context, AttributeSet attrs, int defaultStyle) { super(context, attrs, defaultStyle); initAttributeSet(attrs); density = getResources().getDisplayMetrics().density; final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SlideMenu); mode = typedArray.getInteger(typedArray.getIndex(R.styleable.SlideMenu_slide_mode), MODE_NORMAL); menuWidth = typedArray.getDimensionPixelSize(R.styleable.SlideMenu_slide_menu_width, ToolUnit.dipTopx(300)); slideMenuContent = typedArray.getBoolean(R.styleable.SlideMenu_slide_menu_content_enabled, true); menuAlpha = typedArray.getFloat(R.styleable.SlideMenu_menu_alpha_coefficient, 0.6f); contentAlpha = typedArray.getFloat(R.styleable.SlideMenu_content_alpha_coefficient, 1f); menuScale = typedArray.getFloat(R.styleable.SlideMenu_menu_scale_coefficient, 0.3f); contentScal = typedArray.getFloat(R.styleable.SlideMenu_content_scale_coefficient, 0.8f); menuMove = typedArray.getFloat(R.styleable.SlideMenu_menu_content_move_coefficient, 0.3f); typedArray.recycle(); initContainer(); } /** * 初始化一些属性 * * @param attrs */ private void initAttributeSet(AttributeSet attrs) { if (getChildCount() > 0) { throw new IllegalArgumentException("不允许在布局文件中添加子视图"); } if (null == attrs) { setLayoutParams(new RelativeLayout.LayoutParams(-2, -1)); } //删除ScrollView边界阴影 setHorizontalFadingEdgeEnabled(false); setVerticalFadingEdgeEnabled(false); //删除ScrollView拉到尽头(顶部、底部、左侧、右侧),然后继续拉出现的阴影效果 setOverScrollMode(OVER_SCROLL_NEVER); } /** * @param layoutResId */ public void setBackGround(int layoutResId) { View view = LayoutInflater.from(getContext()).inflate(layoutResId, this, false); setBackGround(view); } /** * @param view */ public void setBackGround(View view) { FrameLayout bgLayout = (FrameLayout) findViewById(BACKGROUND_CONTAINER_ID); bgLayout.addView(view, 0); } /** * @param layoutResId */ public void setMenuBackGround(int layoutResId) { View view = LayoutInflater.from(getContext()).inflate(layoutResId, this, false); setMenuBackGround(view); } /** * @param view */ public void setMenuBackGround(View view) { FrameLayout bgLayout = (FrameLayout) findViewById(MENU_CONTAINER_ID); bgLayout.addView(view, 0); } @Override public void initContainer() { ViewGroup bgLayout = (ViewGroup) createContainer(BACKGROUND_CONTAINER_ID); addView(bgLayout); LinearLayout container = (LinearLayout) createContainer(MAIN_CONTAINER_ID); bgLayout.addView(container); View menu = createContainer(MENU_CONTAINER_ID); View content = createContainer(CONTENT_CONTAINER_ID); container.addView(menu); container.addView(content); } @Override public View createContainer(int id) { View container = new LinearLayout(getContext()); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(-2, -1); if (id == MENU_CONTAINER_ID) { container = new FrameLayout(getContext()); params.width = menuWidth; } else if (id == BACKGROUND_CONTAINER_ID) { container = new FrameLayout(getContext()); } else if (id == CONTENT_CONTAINER_ID) { ((LinearLayout) container).setOrientation(LinearLayout.VERTICAL); params.width = getResources().getDisplayMetrics().widthPixels; } else { ((LinearLayout) container).setOrientation(LinearLayout.HORIZONTAL); } //防止手机休眠唤醒后或其他情况引起scroll自动滚动 container.setFocusable(true); container.setFocusableInTouchMode(true); container.setId(id); container.setLayoutParams(params); return container; } @Override public void setMenu(int layoutId) { ViewGroup left = (ViewGroup) findViewById(MENU_CONTAINER_ID); if (left.getChildCount() > 1) { throw new IllegalArgumentException("菜单视图已存在"); } View menu = LayoutInflater.from(getContext()).inflate(layoutId, null, false); setMenu(menu); } @Override public void setMenu(View menuView) { addChildView(menuView, MENU_CONTAINER_ID); } @Override public void setContent(int layoutId) { ViewGroup right = (ViewGroup) findViewById(CONTENT_CONTAINER_ID); if (right.getChildCount() > 0) { throw new IllegalArgumentException("内容视图已存在"); } View content = LayoutInflater.from(getContext()).inflate(layoutId, null, false); setContent(content); } @Override public void setContent(View contentView) { addChildView(contentView, CONTENT_CONTAINER_ID); } @Override public void addChildView(View view, int id) { ViewGroup container = (ViewGroup) findViewById(id); if (null == container) { throw new NullPointerException("视图容器为空"); } if (null == view.getLayoutParams()) { view.setLayoutParams(new ViewGroup.LayoutParams(-1, -1)); } container.addView(view); } @Override public void onLayoutInit() { loadOnce = true; // 获取左侧布局对象 leftLayout = findViewById(MENU_CONTAINER_ID); // 获取右侧布局对象 rightLayout = findViewById(CONTENT_CONTAINER_ID); scrollTo(menuWidth, 0); } /** * 设置菜单动画模式 * * @param mode */ public void setMode(int mode) { this.mode = mode; } /** * 在onLayout中重新设定左侧布局和右侧布局的参数。 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (changed && !loadOnce) { onLayoutInit(); } } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); //l是当前scroller的相对(scrollview起始位置)坐标,这样计算出来的系数刚好是menu的显示宽度的占比 float scale = l * 1.0f / menuWidth; switch (mode) { case MODE_CONTENT_SCROLL_ONLY: ViewHelper.setTranslationX(leftLayout, menuWidth * scale); if (slideMenuContent) { ViewGroup menu = ((ViewGroup) leftLayout); View menuContent = menu.getChildAt(menu.getChildCount() - 1); //控制menu的顶层视图不滑动,但速度和scroller的速度不等,实现层次移动效果 ViewHelper.setTranslationX(menuContent, -menuWidth * scale * menuMove); } break; case MODE_SCROLL_ALL_WITH_SCALE: float leftScale = 1 - menuScale * scale; float rightScale = contentScal + scale * (1 - contentScal); ViewHelper.setScaleX(leftLayout, leftScale); ViewHelper.setScaleY(leftLayout, leftScale); ViewHelper.setAlpha(leftLayout, menuAlpha + (1 - menuAlpha) * (1 - scale)); ViewHelper.setPivotX(rightLayout, 0); ViewHelper.setPivotY(rightLayout, rightLayout.getHeight() / 2); ViewHelper.setScaleX(rightLayout, rightScale); ViewHelper.setScaleY(rightLayout, rightScale); break; default: break; } ViewHelper.setAlpha(rightLayout, contentAlpha + (1 - contentAlpha) * (1 - scale)); } @Override public boolean onTouchEvent(MotionEvent ev) { int action = ev.getAction(); if (action == MotionEvent.ACTION_UP) { int scrollX = (int) (ev.getRawX() - downX); int slideWidth = menuWidth / BASE_SLIDE_BLOCK; if (scrollX == 0 || scrollX == menuWidth) { return false; } int slideX = Math.abs(scrollX); //右滑 if (scrollX > 0) { if (slideX >= slideWidth) { smoothScrollTo(0, 0); isMenuOpen = true; } else { if (isMenuOpen) { smoothScrollTo(0, 0); } else { smoothScrollTo(menuWidth, 0); } } } //左滑 else if (scrollX < 0) { if (slideX >= slideWidth) { smoothScrollTo(menuWidth, 0); isMenuOpen = false; } else { if (isMenuOpen) { smoothScrollTo(0, 0); } else { smoothScrollTo(menuWidth, 0); } } } //未滑动 else { if (!isMenuOpen) { smoothScrollTo(menuWidth, 0); } else { smoothScrollTo(0, 0); } } downX = 0; downY = 0; return true; } else if (action == MotionEvent.ACTION_MOVE) { if (downX == 0) { downX = ev.getRawX(); } if (downY == 0) { downY = ev.getRawY(); } } return super.onTouchEvent(ev); } /** * 打开菜单 */ public void openMenu() { if (isMenuOpen) { return; } smoothScrollTo(0, 0); isMenuOpen = true; } /** * 关闭菜单 */ public void closeMenu() { if (!isMenuOpen) { return; } smoothScrollTo(menuWidth, 0); isMenuOpen = false; } /** * 切换菜单状态 */ public void toggle() { if (isMenuOpen) { closeMenu(); } else { openMenu(); } } }
一些自定义属性(不要吐槽我的命名(^o^)/)
<declare-styleable name="SlideMenu" > <attr name="slide_mode" format="integer"> <!-- 菜单、内容都一起移动--> <flag name="MODE_NORMAL" value="1"/> <!-- 菜单不移动--> <flag name="MODE_SCROLL_CONTENT" value="2"/> <!-- 菜单、内容移动的同时还缩放menu和content视图--> <flag name="MODE_SCROLL_WIDTH_SCALE" value="3"/> </attr> <!-- 菜单的宽度 dp--> <attr name="slide_menu_width" format="dimension"/> <!-- 菜单内容移动(菜单父视图不动)--> <attr name="slide_menu_content_enabled" format="boolean"/> <!-- 菜单内容移动的系数--> <attr name="menu_content_move_coefficient" format="float"/> <!-- 菜单缩放的比例--> <attr name="menu_scale_coefficient" format="float"/> <!-- 内容缩放的比例--> <attr name="content_scale_coefficient" format="float"/> <!-- 菜单透明的比例--> <attr name="menu_alpha_coefficient" format="float"/> <!-- 内容透明的比例--> <attr name="content_alpha_coefficient" format="float"/> </declare-styleable>
最后,无图无真相啊:
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories