Android-ui效果

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

2017-11-25  本文已影响23人  QiYiFridge

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

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

一. 下拉回弹控件 + 收起

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

功能点分析

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使用者而言是一个灾难。

上一篇下一篇

猜你喜欢

热点阅读