Android自定义View(六)——打造更完美的侧滑

开篇之前,先感谢@鸿洋_,我自定义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:

 /**
     * {@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>

最后,无图无真相啊:

这里写图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值