美文网首页Android-ui效果
交互控件浅解析,安卓View带入门

交互控件浅解析,安卓View带入门

作者: QiYiFridge | 来源:发表于2017-11-25 23:38 被阅读23次

    博主是爱奇艺员工,以上几个都是从爱 奇艺泡泡客户端中截取的。

    本文中一共举出了四个栗子:内容由简到难,但是分析方法和基本原理都是相似的。
    本文四个控件的代码都是笔者自己手写的。希望可以给自己留下些笔记,也给后来者一些启发。

    一. 下拉回弹控件 + 收起

    device-2017-11-25-120023.mp4_1511582458.gif

    功能点分析

    • 下拉手势判定 + View位移
    • 松手之后 + View位移

    View位移推荐使用translationY, 建议在做位移操作时不要直接调用View.setTranslationY()
    而是应该封装一个统一的方法

     public float getCurrentOffset(){
            return getTranslationY();
        }
    
    
        public void setOffset(float targetScrollX){
            //标准坐标轴 右下为正
            //进行左右平移时,需要保证平移的scrollX 范围是 0 - mRefreshView.width()
            targetScrollX = checkOffsetX(targetScrollX);
    
    //        scrollTo(0,(int)targetScrollX);
            setTranslationY(targetScrollX);
        }
    
        private float checkOffsetX(float target) {
            if(target > getMaxOffset() /*|| Math.abs(target - getMaxOffset()) < 10*/){
                target = getMaxOffset();
            }else if(target < 0){
                target = 0;
            }
            return target;
        }
    

    这样的好处是:如果希望修改一种位移方式(例如使用ScrollTo)时,所做的修改量很小。

    核心的事件处理部分:

    
    /*相关变量*/
    
    
        private float mTouchSlop;//最小位移
        /*上一次的点击位置*/
        private float mXDown;
        private float mYDown;
    
      
        private float mYLastMove;//上一次move事件的Y坐标
        private float mYMove;
    
    
      @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if(mHandler!=null && mHandler.shouldForbidden() || ev.getPointerCount() > 1){
                return super.onInterceptTouchEvent(ev);
            }
    
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mXDown = ev.getRawX();
                    mYDown = ev.getRawY();
                    mYLastMove = mYDown;
                    break;
    
                case MotionEvent.ACTION_MOVE:
                    mXMove = ev.getRawX();
                    mYMove = ev.getRawY();
                    float diffX = (mXMove - mXDown);
                    float diffY = (mYMove - mYDown);
    
                    if(Math.abs(diffY) < mTouchSlop || Math.abs(diffY * 0.5) < Math.abs(diffX)){// 过滤掉水平方向的手势
                        break;
                    }
    
                    mYLastMove = mYMove;
                    return true;
            }
            return super.onInterceptTouchEvent(ev);
        }
    
        public float getMaxOffset(){
            return mTargetView.getHeight();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    mYMove = event.getRawY();
    
                    float deltaY =  1.2f * (mYMove - mYLastMove);//正规坐标轴下的偏移
                    setOffset(getCurrentOffset() + deltaY);
                    mYLastMove = mYMove;
                    break;
                case MotionEvent.ACTION_UP:
                    // 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
                    onRelease();
                    break;
            }
            return true;
        }
    
        public float getCurrentOffset(){
            return getTranslationY();
        }
    
    

    整体思路还是按照View的动作拦截机制完成的。
    在onInterceptTouchEvent进行动作判别、拦截。
    在onTouchEvnet中完成偏移量计算、View的位移、以及回弹动画的播放。

    回弹动画

      public void onRelease(){
            final boolean hasGotPoint = Math.abs(getCurrentOffset()) >= mTriggerPoint;
            mAnimator = ValueAnimator.ofFloat(getCurrentOffset(), hasGotPoint? getMaxOffset() : 0).setDuration(ANIMATOR_DURATION);
            mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float animatedValue = (float)animation.getAnimatedValue();
                    setOffset(animatedValue);
                }
            });
            mAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {
    
                }
    
                @Override
                public void onAnimationEnd(Animator animation) {
                        //todo 进入详情页
                        if(mListener!=null && hasGotPoint) {
                            mListener.onTriggered();
                        }
                }
    
                @Override
                public void onAnimationCancel(Animator animation) {
    
                }
    
                @Override
                public void onAnimationRepeat(Animator animation) {
    
                }
            });
            mAnimator.start();
        }
    
    

    二. 视频缩放 + View动画

    device-2017-11-25-120156.mp4_1511582541.gif

    这个效果看起来稍微复杂,但是基本实现思路是类似的
    1.找到合适的动作触发时机
    2.对View进行操作

    除此之外还有几个点需要注意:

    1.从上图可以看到视频的主要形态有三种,100%,80%以及隐藏。状态的跳转需要记录。
    由于这个view的动画基本上是只要触发就会进行下去的。

    1. 内部还有个ListView。需要处理好和ListView的冲突。

    3.另外,由于动作几乎是立即触发并且不可逆的(施加动作之后就会执行形变)
    所以,我们只在onInterceptTouchEvnet中就可以完成主要逻辑了。

       @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (listView == null || videoLayout == null) {//子控件还未初始化
                return super.onInterceptTouchEvent(ev);
            }
            if (!enable) {//禁用开关
                return super.onInterceptTouchEvent(ev);
            }
    
            //操作区域在listView以上,即视频区域内
            int y = (int) ev.getRawY();
            int x = (int) ev.getRawX();
    
            int[] location = new int[2];
            listView.getLocationOnScreen(location);
            if (y < location[1]) {
                return super.onInterceptTouchEvent(ev);
            }
    
            if (isAnimationPlaying) {
                return true;//动画播放期间禁止操作
            }
    
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    // 发生down事件时,记录y坐标
                    mLastMotionY = y;
                    mLastMotionX = x;
                    break;
    
                case MotionEvent.ACTION_MOVE:
                    deltaY = y - mLastMotionY;
                    if (Math.abs(deltaY) < 20) {
                        break;
                    }
                    if (!isVideoStop() && isListViewTopping()) {
                        //非暂停态
                        if (deltaY < 0 && videoState == VIDEO_LAYOUT_NORMAL_SIZE) {
                            zoomInVideoLayout(VIDEO_LAYOUT_HALF_SIZE);
                            return true;
                        } else if (deltaY > 0 && videoState != VIDEO_LAYOUT_NORMAL_SIZE) {
                            zoomInVideoLayout(VIDEO_LAYOUT_NORMAL_SIZE);
                            return true;
                        }
                    }
    
                    if (isVideoStop()) {
                        if (deltaY < 0 && videoState != VIDEO_LAYOUT_ZERO_SIZE) {
                            zoomInVideoLayout(VIDEO_LAYOUT_ZERO_SIZE);
                            return true;
                        } else if (deltaY > 0 && isListViewTopping()) {
                            if (videoState == VIDEO_LAYOUT_ZERO_SIZE) {
                                zoomInVideoLayout(VIDEO_LAYOUT_NORMAL_SIZE);
                                return true;
                            }
                        }
                    }
                    break;
            }
            return super.onInterceptTouchEvent(ev);
        }
    
    

    三. 左拉刷新

    从原理上来讲,这个控件其实和常见的下拉刷新控件是一样的。只是方向变为了向左滑动。

    device-2017-11-25-224157.mp4_1511620973.gif

    完全从零做起的,实现一个这个小控件也是挺有意思的。

    主要思路是,在视觉区域以外的地方添加一个新View(indicate 刷新状态)
    主要动作是对整个View做位移动画。

      @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            mTargetView.layout(l,t,r,b);//在此栗子中是图片
            mRefreshView.layout(r,t,r + mRefreshView.getMeasuredWidth(),b);//左拉提示,旋转指示等
        }
    

    而动作判别又是我们熟悉的那一套代码啦

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if(mHandler!=null && mHandler.shouldForbidden()){
                return super.onInterceptTouchEvent(ev);
            }
    
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mXDown = ev.getRawX();
                    mYDown = ev.getRawY();
                    mXLastMove = mXDown;
                    break;
                case MotionEvent.ACTION_MOVE:
                    mXMove = ev.getRawX();
                    mYMove = ev.getRawY();
                    float diffX = (mXMove - mXDown);
                    float diffY = (mYMove - mYDown);
    
                    if(Math.abs(diffX * 0.5) < Math.abs(diffY)){
                        break;
                    }
    
                    mXLastMove = mXMove;
                    // 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件
                    //向左滑动
                    if (diffX < 0  && Math.abs(diffX) > mTouchSlop && !canTargetScrollLeft()) {
                        return true;
                    }else if(diffX > 0 && Math.abs(diffX) > mTouchSlop && isRefreshViewDisplayed()){
                        return true;
                    }
                    break;
            }
            return super.onInterceptTouchEvent(ev);
        }
    
    
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    mXMove = event.getRawX();
    
                    float diffX =  1.6f * (mXMove - mXLastMove);//正规坐标轴下的偏移
                    diffX = diffX * (1.2f - (getCurrentOffset()/getMaxOffset()));//阻尼修正
    
                    float target = checkOffsetX(getCurrentOffset()- diffX);
    
    
                    if(getMaxOffset() * mPercentFactor < target){
                        mRefreshView.setExplodeState(true);//爆炸特效 + 提示转换
                    }else{
                        mRefreshView.setExplodeState(false);
                    }
    
                    setOffset(target);
                    mXLastMove = mXMove;
                    break;
                case MotionEvent.ACTION_UP:
                    // 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
                    //todo 进入详情页
                    if(mListener!=null && mRefreshView.isHasExploded()) {
                        mListener.onTriggered();
                    }
                    postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            onRelease();
                        }
                    }, mRefreshView.isHasExploded() ? 500 :0);
                    break;
            }
            return super.onTouchEvent(event);
        }
    
    

    主要动作核心代码:

        public void setOffset(float targetScrollX){
            //标准坐标轴 右下为正
            //进行左右平移时,需要保证平移的scrollX 范围是 0 - mRefreshView.width()
            targetScrollX = checkOffsetX(targetScrollX);
            float percent = (targetScrollX / getMaxOffset())/ mPercentFactor;
            percent = Math.min(percent,1);
            mRefreshView.updatePullPercent(percent);
            scrollTo((int)targetScrollX,0);
        }
    
        private float checkOffsetX(float targetScrollX) {
            if(targetScrollX > mRefreshView.getWidth() /*|| Math.abs(targetScrollX - getMaxOffset()) < 10*/){
                targetScrollX = mRefreshView.getWidth();
            }else if(targetScrollX < 0){
                targetScrollX = 0;
            }
            return targetScrollX;
        }
    
    

    被刷新的View被抽象出来作为mRefreshView,相对比较简单,只要实现了

    void  updatePullPercent(float percent);
    void setExploedState(boolean explored); 
    

    这里除了问题提示之外,还有一个
    旋转的箭头以及渐变的绿色背景。

    箭头是现成的UI图,绿色背景稍微麻烦一些,需要使用颜色渐变来完成。

    下面的RotateArrowView 实现了这个功能,顺便将箭头也add了进来。

    //只包括了这个类的核心代码
    public class RotateArrowView extends FrameLayout {
    
    
        private ArgbEvaluator argbEvaluator = new ArgbEvaluator();
    
        ...
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int x = getMeasuredWidth()/2;
            int y = getMeasuredHeight()/2;
            int radius = getWidth()/2;
            canvas.drawCircle(x,y,radius,mPaint);
        }
    
        public void updatePercent(float percent){
            int evaluateColor = (int)argbEvaluator.evaluate(percent, startColor, endColor);
            mPaint.setColor(evaluateColor);
            arrow.setRotation(180* percent);//箭头的角度需要旋转
            postInvalidate();
        }
    }
    

    ArgbEvaluator 是谷歌提供的一个方便的颜色渐变计算器。

    之前对ViewGroup在直觉上有个误解,就是复写父view的onDraw要考虑和子View z-index上的层级关系。
    实际上ViewGroup的onDraw复写之后,并不会影响到其子View(只是默默地在最后面画了一个背景)。

    其实思考一下也是,父View以及子View的z-index层级关系是在layout时就已经确定好的。如果需要在onDraw再去费心考虑,对于api使用者而言是一个灾难。

    相关文章

      网友评论

        本文标题:交互控件浅解析,安卓View带入门

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