美文网首页UI效果仿写view自定义
自定义ScaleLayout (模仿小米相册查看图片效果)

自定义ScaleLayout (模仿小米相册查看图片效果)

作者: _deadline | 来源:发表于2016-10-20 07:37 被阅读1883次

    之前写的两篇关于自定义View:
    http://www.jianshu.com/p/32d7d1ab985c 模仿饿了么加载效果(五八同城,UC也都有这个效果)

    http://www.jianshu.com/p/e180aa9f293b 模仿小米的进度控件

    先来看看效果图,这个gif弄得蛋疼,加快了播放速度,降低了清晰度:


    scale_gif1.gif
    GIF_20161019_230600.gif

    github地址:https://github.com/niniloveyou/ScaleLayout

    下面会从以下几个方面分析如何实现这个效果:

    1.初始化完成后做了什么

    2.onMeasure onLayout

    3.触摸事件的处理

    4.对外提供方法和接口

    .
    .
    .
    .
    首先讲讲大概的思路:
    就是我们要有三个View 分别为TopView CenterView bottomView 这很好理解,故名思义就是把这三个子View分别放在ViewGroup的上中下。
    OnMeasure()中把CenterView的大小设置为等同于自身的大小
    onLayout() 获取topview bottomView的高度,根据高度设置当centerView缩小时topView/BottomView位移距离
    onInterceptTouchEvent() 只处理滑动冲突部分。
    onTouchEvent()中才是真正滑动缩小或放大实现的部分。

    1.初始化完成后做了什么

    我们先贴代码,后面紧跟着解释:

    <pre>
    @Override
    protected void onFinishInflate() {
    super.onFinishInflate();

        int childCount = getChildCount();
        if(childCount < 1){
            throw new IllegalStateException("ScaleLayout should have one direct child at least !");
        }
    
        mTopView = findViewById(R.id.scaleLayout_top);
        mBottomView = findViewById(R.id.scaleLayout_bottom);
        mCenterView = findViewById(R.id.scaleLayout_center);
    
        // if centerView does not exist
        // it make no sense
        if(mCenterView == null){
            throw new IllegalStateException("ScaleLayout should have one direct child at least !");
        }
    
        LayoutParams lp = (FrameLayout.LayoutParams)mCenterView.getLayoutParams();
        lp.gravity &= Gravity.CENTER;
        mCenterView.setLayoutParams(lp);
    
        //hide topView and bottomView
        //set the topView on the top of ScaleLayout
        if(mTopView != null){
            lp = (FrameLayout.LayoutParams)mTopView.getLayoutParams();
            lp.gravity &= Gravity.TOP;
            mTopView.setLayoutParams(lp);
            mTopView.setAlpha(0);
        }
    
        //set the bottomView on the bottom of ScaleLayout
        if(mBottomView != null){
            lp = (FrameLayout.LayoutParams)mBottomView.getLayoutParams();
            lp.gravity &= Gravity.BOTTOM;
            mBottomView.setLayoutParams(lp);
            mBottomView.setAlpha(0);
        }
    
        setState(mState, false);
    }
    

    </pre>

    大家都知道onFinishInflate方法是View在XML中解析完成的回调,因此可以在里面做一些检查以及初始化的工作。 从代码不难看出,我首先就是检查了ScaleLayout的子View数量, 少于一个就直接抛出异常了,因为如果没有一个子View, 咱们自定义的这个ScaleLayout就没什么意义了, 其次 是我指定了 上中下三个子View的id, 这么做是因为ScaleLayout是个ViewGroup,可能不止三个,但是多了我们又没法判断,哪一个是topView, 哪个是centerView, 有可能会乱掉。

    后面又对CenterView做了判空, 以及对三个View的位置做了些设置。

    2.onMeasure onLayout

    <pre>
    /**
    * 使得centerView 大小等同ScaleLayout的大小
    * 如果不想这样处理,也可以在触摸事件中使用TouchDelegate
    * @param widthMeasureSpec
    * @param heightMeasureSpec
    */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
        int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
        int layoutWidth = widthSize - getPaddingLeft() - getPaddingRight();
    
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(layoutWidth, MeasureSpec.EXACTLY);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.EXACTLY);
    
        mCenterView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
    
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    
        if(mBottomView != null){
            mBottomViewMoveDistance = mBottomView.getMeasuredHeight();
        }
    
        if(mTopView != null){
            mTopViewMoveDistance = mTopView.getMeasuredHeight();
        }
    
        if(mSuggestScaleEnable){
            setMinScale(getSuggestScale());
        }
    }
    

    </pre>
    很简单,只说一点:mBottomViewMoveDistance, mTopViewMoveDistance 分别为bottomView, topView动画时位移的距离。

    3.触摸事件的处理

    重点来了这个也是核心部分了。

    onInterceptTouchEvent

    <pre>

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    
        boolean intercept = false;
    
        switch (ev.getAction()) {
    
            case MotionEvent.ACTION_DOWN:
    
                onTouchEvent(ev);
                mInitialMotionX = ev.getX();
                mInitialMotionY = ev.getY();
                break;
    
            case MotionEvent.ACTION_MOVE:
                final float deltaX = Math.abs(ev.getX() - mInitialMotionX);
                final float deltaY = Math.abs(ev.getY() - mInitialMotionY);
    
                if(mCanScaleListener != null
                        && !mCanScaleListener.onGetCanScale(ev.getX() - mInitialMotionX > 0)){
                    intercept = false;
                }else {
                    intercept = deltaY > deltaX && deltaY > mTouchSlop;
                }
                break;
        }
        return intercept;
    }
    

    </pre>
    所有的down事件都不拦截,因此接下来的move, up事件,
    都会先执行onInterceptTouchEvent的(move, up)
    继而分发给子view的dispatchTouchEvent(move, up),
    因此在onInterceptTouchEvent(move)事件中我们可以判断是否满足滑动条件,满足就拦截,拦截了之后move up事件就会都分发给自身的OnTouchEvent, 否则如上继续分发给子View.

    intercept = deltaY > deltaX && deltaY > mTouchSlop;
    

    即Y位移的距离大于X方向 ,并且Y方向位移的距离大于TouchSlop,则认为这是有效滑动。

     /**
         * 返回是否可以scale,主要为了适配部分有滑动冲突的view
         * 如TouchImageView, 甚至webView等
         * isScrollSown = true  代表向下,
         * isScrollSown = false 代表向上
         */
        public interface OnGetCanScaleListener{
    
            boolean onGetCanScale(boolean isScrollSown);
        }
    
    
    if(mCanScaleListener != null 
              && !mCanScaleListener.onGetCanScale(ev.getX() - mInitialMotionX > 0)){ 
      intercept = false;
    }
    

    这下明白了吧,我是做了个接口,要不要拦截由你说了算,也算我偷懒了。

    OnTouchEvent

     /**
         * 该方法中实现了
         * 上滑缩小下滑放大功能
         * 也可设置为 上滑放大下滑缩小
         * @param ev
         * @return
         */
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
    
            if (!isEnabled() || !mSlideScaleEnable) {
                return super.onTouchEvent(ev);
            }
    
    
    
            switch (ev.getActionMasked()) {
    
                case MotionEvent.ACTION_DOWN:
                    downY = ev.getY();
                    return true;
    
                case MotionEvent.ACTION_MOVE:
                    if(mCanScaleListener != null && !mCanScaleListener.onGetCanScale(ev.getY() - downY > 0)){
                        return super.onTouchEvent(ev);
                    }
                    if (Math.abs(ev.getY() - downY) > mTouchSlop) {
    
                        mSlopLength += (ev.getY() - downY);
    
                        float scale;
                        if (mSlideUpOrDownEnable) {
    
                            scale = 1 + (0.8f * mSlopLength / getMeasuredHeight());
                        } else {
                            scale = 1 - (0.8f * mSlopLength / getMeasuredHeight());
                        }
    
                        scale = Math.min(scale, 1f);
    
                        mCurrentScale = Math.max(mMinScale, scale);
    
                        doSetScale();
    
                        downY = ev.getY();
                    }
    
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    if (mCurrentScale > mMinScale && mCurrentScale < 1f) {
    
                        float half = (1 - mMinScale) / 2;
    
                        if (mCurrentScale >= mMinScale + half) {
    
                            setState(STATE_CLOSE, true);
                        } else {
    
                            setState(STATE_OPEN, true);
                        }
                    }
                    break;
            }
    
            return super.onTouchEvent(ev);
        }
    
    

    这部分,首先是move的时候用mSlopLength计算滑动的距离向下滑就加正值,向上划值就减小,不断根据这个值计算当前的Scale. 应该缩放的比例,然后根据这个值计算topView bottomView 的透明度,位移距离,等等, 当UP的时候,根据当前的Scale决定是应该放大到原宽高还是缩小,以动画的形式。

    ···
    /**
    * 1.触发监听事件
    * 2.计算scale的pivotX, pivotY(因为topView 和bottomView 的高度可能不一样,所以不能固定设置在中心点)
    * 3.设置 mCenterView的scale
    * 4.设置topView and BottomView 的动画(渐变和位移)
    */
    private void doSetScale() {

        int scaleListenerCount = mScaleListenerList.size();
    
        OnScaleChangedListener mScaleChangedListener;
        for (int i = 0; i < scaleListenerCount; i++) {
            mScaleChangedListener = mScaleListenerList.get(i);
            if(mScaleChangedListener != null){
                mScaleChangedListener.onScaleChanged(mCurrentScale);
            }
        }
    
        if(mCurrentScale == mMinScale || mCurrentScale == 1f){
            int stateListenerCount = mStateListenerList.size();
    
            OnStateChangedListener mStateChangedListener;
            for (int i = 0; i < stateListenerCount; i++) {
                mStateChangedListener = mStateListenerList.get(i);
                if(mStateChangedListener != null){
                    mStateChangedListener.onStateChanged(mCurrentScale == mMinScale);
                }
            }
        }
    
        doSetCenterView(mCurrentScale);
        doSetTopAndBottomView(mCurrentScale);
    }
    

    ···

    我把监听事件也贴上:

        /**
         * 当centerView 的scale变化的时候,通过这个
         * 接口外部的View可以做一些同步的事情,
         * 比如,你有一个其他的view要根据centerView的变化而变化
         */
        public interface OnScaleChangedListener{
    
            void onScaleChanged(float currentScale);
        }
    
        /**
         * state == false 当完全关闭(scale == 1f)
         * state == true  或当完全开启的时候(scale = mMinScale)
         */
        public interface OnStateChangedListener{
    
            void onStateChanged(boolean state);
        }
    

    4.对外提供方法和接口

    关于接口,代码我都无耻的贴上去了。

    下面说说提供的几个简单的外部方法:

        /**
         * 设置最小scale
         * {@link #DEFAULT_MIN_SCALE}
         * @param minScale
         */
        public void setMinScale(float minScale){
    
            if(minScale > 0f && minScale < 1f){
                if(mMinScale != minScale){
                    if(isOpen()){
                        if(animator != null){
                            animator.cancel();
                            animator = null;
                        }
                        animator = getAnimator(mMinScale, minScale);
                        animator.start();
                    }
                    mMinScale = minScale;
                }
            }
        }
    
    
        public float getMinScale(){
            return mMinScale;
        }
    
        public float getCurrentScale(){
            return mCurrentScale;
        }
    
    
        public void setSuggestScaleEnable(boolean enable){
            if(mSuggestScaleEnable != enable){
                mSuggestScaleEnable = enable;
                requestLayout();
            }
        }
    
        /**
         * 设置的scale不得当的话,有可能topView / bottomView被覆盖
         * 通过设置{@link #setSuggestScaleEnable(boolean)}启用
         * @return
         */
        private float getSuggestScale(){
    
            int height = 0;
    
            if(mTopView != null){
                height += mTopView.getMeasuredHeight();
            }
    
            if(mBottomView != null){
                height += mBottomView.getMeasuredHeight();
            }
            return 1 - height * 1f / (getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
        }
    
    
        /**
         * 设置是否启用滑动缩小功能
         * @param enable
         */
        public void setSlideScaleEnable(boolean enable){
            this.mSlideScaleEnable = enable;
        }
    
        /**
         *   现在有这么几种情况, 默认第二种, 两者都可以的话,感觉好奇怪,
         *   比如一直下滑会由大变小后又变大,操作感觉不是很好
         *   1. 只上滑放大下滑缩小  false
         *   2. 只上滑缩小下滑放大  true
         */
        public void setSlideUpOrDownEnable(boolean enable){
            this.mSlideUpOrDownEnable = enable;
        }
    
        /**
         * add OnScaleChangedListener
         * @param listener
         */
        public void addOnScaleChangedListener(OnScaleChangedListener listener){
            if(listener != null){
                mScaleListenerList.add(listener);
            }
        }
    
        /**
         * add OnStateChangedListener
         * @param listener
         */
        public void addOnStateChangedListener(OnStateChangedListener listener){
            if(listener != null){
                mStateListenerList.add(listener);
            }
        }
    
        public void setOnGetCanScaleListener(OnGetCanScaleListener listener){
            mCanScaleListener = listener;
        }
    
        /**
         *  {@link #setState(int state, boolean animationEnable)}
         * @param state
         */
        public void setState(int state){
            setState(state, true);
        }
    
        /**
         * 设置状态变化
         * @param state open or close
         * @param animationEnable change state with or without animation
         */
        public void setState(final int state, boolean animationEnable) {
    
            if(!animationEnable)
            {
                if(state == STATE_CLOSE){
                    mSlopLength = 0;
                    mCurrentScale = 1;
                }else{
                    if(mSlideUpOrDownEnable) {
                        mSlopLength = -getMeasuredHeight() * (1 - mMinScale) * 1.25f;
                    }else{
                        mSlopLength = getMeasuredHeight() * (1 - mMinScale) * 1.25f;
                    }
                    mCurrentScale = mMinScale;
                }
                doSetScale();
                mState = state;
    
            }else{
                if(animator != null){
                    animator.cancel();
                    animator = null;
                }
    
                if(state == STATE_CLOSE && mCurrentScale != 1){
    
                    mSlopLength = 0;
                    animator = getAnimator(mCurrentScale, 1f);
    
                }else if(state == STATE_OPEN && mCurrentScale != mMinScale){
    
                    if(mSlideUpOrDownEnable) {
                        mSlopLength = -getMeasuredHeight() * (1 - mMinScale) * 1.25f;
                    }else{
                        mSlopLength = getMeasuredHeight() * (1 - mMinScale) * 1.25f;
                    }
                    animator = getAnimator(mCurrentScale, mMinScale);
                }
    
                if(animator != null) {
                    animator.addListener(new AnimatorListenerAdapter() {
    
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            mState = state;
                        }
    
                    });
                    animator.start();
                }
            }
        }
    
        /**
         * 获取当前状态开启或者关闭
         * @return
         */
        public boolean isOpen(){
    
            return mState == STATE_OPEN;
        }
    
    

    代码贴完了。

    如果感觉还行,到我的github star一下吧。 谢谢!

    https://github.com/niniloveyou/ScaleLayout

    相关文章

      网友评论

        本文标题:自定义ScaleLayout (模仿小米相册查看图片效果)

        本文链接:https://www.haomeiwen.com/subject/cetfuttx.html