动画库NineOldAndroids实战自定义悬浮窗

632 阅读3分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

前言

我们项目中一直有一个 提现功能,在实际的效果中就是一个可以拖动的红包View,点击后就是响应的逻辑

最近一个国际版客户就提出来想要使用,我当时还纳闷了,谷歌商店和外国客户吃这一套吗?不管了客户就是爷,开整.

先看效果

动画.gif

这个红色的就是我们项目中的一个效果,好像是我们目前的Android老大哥封装的,代码有点乱,我没咋看明白.大家可以看到最后上划时卡住了一下,这个不用在意,这是投影软件的问题.

这个View在使用中有点问题,我们主页上面有一个banner,每当你手指拖动时只要上方banner切换,就会立马卡到最左或者最右.十分诡异.我们程序员是不可能容忍这种事情发生的,立马开始改造!!!

首先贴出来老代码,大家来琢磨琢磨

/**
 * 红包悬浮窗
 */
public class FloatDragView {

    // 屏幕的宽度 屏幕的高度
    private static int mScreenWidth = -1, mScreenHeight = -1;
    // 用于记录上一次的位置(坐标0对应x,坐标1对应y)
    private static int[] lastPosition;
    // 上下文
    private final Activity context;
    // 可拖动按钮(外层布局)
    private RelativeLayout mImageView;
    // 是否截断touch事件
    private boolean isIntercept = false;
    // 控件宽高
    private int mImageViewWidth, mImageViewHeight;
    // 控件相对屏幕左上角移动的位置
    private int relativeMoveX, relativeMoveY;

    /**
     * 初始化实例
     *
     * @param context
     */
    public FloatDragView(Activity context) {
        this.context = context;
        mScreenWidth = ScreenSizeUtils.getInstance(context).getScreenWidth();
        mScreenHeight = ScreenSizeUtils.getScreenHeight(context);
        lastPosition = new int[]{0, 0};
    }

    /**
     * @param context        上下文
     * @param mViewContainer 可拖动按钮要存放的对应的Layout
     * @param clickListener  可拖动按钮的点击事件
     */
    public void addFloatDragView(Activity context, RelativeLayout mViewContainer, View.OnClickListener clickListener) {
        // 设置宽高
        mImageViewWidth = ImageUtil.dp2px(context, 55);
        mImageViewHeight = mImageViewWidth * 136 / 107;
        // 获取拖动按钮
        mImageView = getFloatDragView(clickListener);
        if (!getChildA(mViewContainer)) {
            mViewContainer.addView(mImageView);
            // 设置拖动按钮,并添加在父布局里
            RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) mImageView.getLayoutParams();
            layoutParams.width = mImageViewWidth;
            layoutParams.height = mImageViewHeight;
            mImageView.setLayoutParams(layoutParams);
        }
    }

    /**
     * @param clickListener
     * @return 获取可拖动按钮的实例
     */
    private RelativeLayout getFloatDragView(View.OnClickListener clickListener) {
        if (mImageView != null && mImageView.getChildCount() != 0) {
            if (mImageView.getVisibility() != View.VISIBLE) {
                mImageView.setVisibility(View.VISIBLE);
            }
            return mImageView;
        } else {
            if (mImageView == null) {
                mImageView = new RelativeLayout(context);
                mImageView.setTag("123456789");
            }
            mImageView.setVisibility(View.VISIBLE);
            mImageView.setClickable(true);
            mImageView.setFocusable(true);
            if (clickListener != null) {
                mImageView.setOnClickListener(clickListener);
            }
            // 添加图片控件
            ImageView imageView = new ImageView(context);
            imageView.setClickable(false);
            imageView.setFocusable(false);
            imageView.setEnabled(false);
            imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
            imageView.setImageResource(R.mipmap.float_red_package);
            RelativeLayout.LayoutParams imageParams = new RelativeLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            mImageView.addView(imageView, imageParams);
            // 初始位置
            RelativeLayout.LayoutParams lpFeedback = new RelativeLayout.LayoutParams(
                    RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
            lpFeedback.setMargins(0, 0, 15, 250);
            lpFeedback.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
            lpFeedback.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
            lpFeedback.width = mImageViewWidth;
            lpFeedback.height = mImageViewHeight;
            mImageView.setLayoutParams(lpFeedback);
            // 设置拖动
            setFloatDragViewTouch(mImageView);
            return mImageView;
        }
    }

    /**
     * 可拖动按钮的touch事件
     *
     * @param floatDragView
     */
    @SuppressLint("ClickableViewAccessibility")
    private void setFloatDragViewTouch(final RelativeLayout floatDragView) {
        floatDragView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(final View v, MotionEvent event) {
                int action = event.getAction();
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        isIntercept = false;
                        relativeMoveX = (int) event.getRawX();
                        relativeMoveY = (int) event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int dx = (int) event.getRawX() - relativeMoveX;
                        int dy = (int) event.getRawY() - relativeMoveY;
                        // 这里修复一些华为手机无法触发点击事件
                        int distance = (int) Math.sqrt(dx * dx + dy * dy);
                        // 此处稍微增加一些移动的偏移量,防止手指抖动,误判为移动无法触发点击时间
                        if (distance == 0) {
                            isIntercept = false;
                            Log.e("TAG", "isIntercept: ");
                            break;
                        }
                        isIntercept = true;
                        int left = v.getLeft() + dx;
                        int top = v.getTop() + dy;
                        int right = v.getRight() + dx;
                        int bottom = v.getBottom() + dy;
                        // 范围判断
                        if (left < 15) {
                            left = 15;
                            right = left + v.getWidth();
                        }
                        if (right > mScreenWidth - 15) {
                            right = mScreenWidth - 15;
                            left = right - v.getWidth();
                        }
                        if (top < 250) {
                            top = 250;
                            bottom = top + v.getHeight();
                        }
                        if (bottom > mScreenHeight - 250) {
                            bottom = mScreenHeight - 250;
                            top = bottom - v.getHeight();
                        }
                        Log.e("TAG", "onTouch: "+left+"   "+top+"   "+right+"   "+bottom);
                        v.layout(left, top, right, bottom);
                        relativeMoveX = (int) event.getRawX();
                        relativeMoveY = (int) event.getRawY();
                        break;
                    case MotionEvent.ACTION_UP:
                        if (isIntercept) {
                            // 每次移动都要设置其layout,不然由于父布局可能嵌套listview,
                            // 当父布局发生改变冲毁(如下拉刷新时)则移动的view会回到原来的位置
                            RelativeLayout.LayoutParams lpFeedback = new RelativeLayout.LayoutParams(
                                    RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
                            lpFeedback.setMargins(v.getLeft(), v.getTop(), 0, 0);
                            lpFeedback.width = mImageViewWidth;
                            lpFeedback.height = mImageViewHeight;
                            v.setLayoutParams(lpFeedback);
                            // 设置靠近边沿的
                            setImageViewNearEdge(v);
                        }
                        break;
                }
                return isIntercept;
            }
        });
    }

    /**
     * 将拖动按钮移动到边沿
     *
     * @param v
     */
    private void setImageViewNearEdge(final View v) {
        Log.e("TAG", "setImageViewNearEdge: "+v.getLeft());
        if (v.getLeft() < (mScreenWidth / 2)) {
            // 设置位移动画 向左移动控件位置
            final TranslateAnimation animation = new TranslateAnimation(0, -v.getLeft() + 15, 0, 0);
            animation.setDuration(400);// 设置动画持续时间
            animation.setRepeatCount(0);// 设置重复次数
            animation.setFillAfter(true);
            animation.setRepeatMode(Animation.ABSOLUTE);
            animation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation arg0) {
                    // TODO: 2017/3/1
                }

                @Override
                public void onAnimationRepeat(Animation arg0) {
                    // TODO: 2017/3/1
                }

                @Override
                public void onAnimationEnd(Animation arg0) {
                    v.clearAnimation();
                    RelativeLayout.LayoutParams lpFeedback = new RelativeLayout.LayoutParams(
                            RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
                    lpFeedback.setMargins(15, v.getTop(), 0, 0);
                    lpFeedback.width = mImageViewWidth;
                    lpFeedback.height = mImageViewHeight;
                    v.setLayoutParams(lpFeedback);
                    v.postInvalidateOnAnimation();
                    lastPosition[0] = 0;
                    lastPosition[1] = v.getTop();
                }
            });
            v.startAnimation(animation);
        } else {
            final TranslateAnimation animation = new TranslateAnimation(0,
                    mScreenWidth - v.getLeft() - v.getWidth() - 15, 0, 0);
            animation.setDuration(400);// 设置动画持续时间
            animation.setRepeatCount(0);// 设置重复次数
            animation.setRepeatMode(Animation.ABSOLUTE);
            animation.setFillAfter(true);
            animation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation arg0) {
                    // TODO: 2017/3/1
                }

                @Override
                public void onAnimationRepeat(Animation arg0) {
                    // TODO Auto-generated method stub
                }

                @Override
                public void onAnimationEnd(Animation arg0) {
                    v.clearAnimation();
                    RelativeLayout.LayoutParams lpFeedback = new RelativeLayout.LayoutParams(
                            RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
                    lpFeedback.setMargins(mScreenWidth - v.getWidth() - 15, v.getTop(), 0, 0);
                    lpFeedback.width = mImageViewWidth;
                    lpFeedback.height = mImageViewHeight;
                    v.setLayoutParams(lpFeedback);
                    v.postInvalidateOnAnimation();
                    lastPosition[0] = mScreenWidth - v.getWidth();
                    lastPosition[1] = v.getTop();
                }
            });
            v.startAnimation(animation);
        }
    }

    public void remove() {
        if (mImageView != null) {
            mImageView.removeAllViews();
            mImageView.setVisibility(View.GONE);
        }
    }

    private Boolean getChildA(View view) {
        Boolean a = false;
        if (view instanceof ViewGroup) {
            ViewGroup vp = (ViewGroup) view;
            for (int i = 0; i < vp.getChildCount(); i++) {
                View viewchild = vp.getChildAt(i);
                if (viewchild.getTag() != null && String.valueOf(viewchild.getTag()).equals("123456789")) {
                    return true;
                }
                a = a || getChildA(viewchild);
            }
        }
        return a;
    }
}

用法

image.png

直接new一个,然后调用addFloatDragView,有三个参数,第一个是上下文,第二个是RelativeLayout,第三个就是点击监听.

这个用法比较限制,你的根布局必须是RelativeLayout,不然没法用.你也可以去里面吧所有RelativeLayout替换成ConstraintLayout,你会发现这个View直接废掉,移动动画不好使了,而且固定在左上角.这种情况不要在基础上改了,直接自定义都比修改省时间.

下面是我随便写的一个View

/**
 * 滑动悬浮窗
 */
public class TestImageView extends AppCompatImageView {

    private int mScreenWidth;
    private int mScreenHeight;
    private int mLastX;
    private int mLastY;
    private boolean isMove = false;
    private OnClickListener onClickListener;

    private Activity activity;

    public TestImageView(@NonNull @NotNull Context context) {
        super(context);
        init(context);
    }

    public TestImageView(@NonNull @NotNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);

    }

    public TestImageView(@NonNull @NotNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);

    }

    public void setActivity(Activity activity) {
        this.activity = activity;
    }

    private void init(Context context) {
        mScreenWidth = ScreenSizeUtils.getInstance(context).getScreenWidth();
        mScreenHeight = ScreenSizeUtils.getScreenHeight(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        // 当前手指坐标
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                isMove = false;
                break;
            case MotionEvent.ACTION_MOVE:
                isMove = true;
                int deltaX = x - mLastX; // x方向移动量
                int deltaY = y - mLastY; // y方向移动量
                int translationX = (int) (ViewHelper.getTranslationX(this) + deltaX); // x方向平移deltaX
                int translationY = (int) (ViewHelper.getTranslationY(this) + deltaY); // y方向平移deltaY
                ViewHelper.setTranslationX(this, translationX);
                ViewHelper.setTranslationY(this, translationY);


                break;
            case MotionEvent.ACTION_UP:
                /**
                 * 当手指点击后抬起,且未滑动就会触发点击监听,只要滑动1像素就会取消点击监听
                 */
                if (!isMove) {
                    if(onClickListener!=null){
                        onClickListener.onClick(this);
                    }
                }
                setImageViewNearEdge(this);
                break;
        }
        // 更新位置
        mLastX = x;
        mLastY = y;
        return true;
    }

    private void setImageViewNearEdge(final View v) {
        ViewPropertyAnimator viewPropertyAnimator = ViewPropertyAnimator.animate(this);
        //设置动画时间
        viewPropertyAnimator.setDuration(400);
        //当控件移动并且抬起手指时,判断这个控件最后的位置是偏左还是偏右
        if (mLastX < (mScreenWidth / 2)) {
            // 设置位移动画 向左移动控件位置
            viewPropertyAnimator.translationX(-v.getLeft() + 15);
        } else {
            // 设置位移动画 向右移动控件位置
            viewPropertyAnimator.translationX(mScreenWidth - v.getLeft() - v.getWidth() - 15);
        }

        viewPropertyAnimator.start();
    }

    @Override
    public void setOnClickListener(OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }
}

对了,如果你要是用这个View你需要导入一个三方库 NineOldAndroids

官方解释: Android library for using the Honeycomb (Android 3.0) animation API on all versions of the platform back to 1.0!Animation prior to Honeycomb was very limited in what it could accomplish so in Android 3.x a new API was written. With only a change in imports, we are able to use a large subset of the new-style animation with exactly the same API.

大概意思就是能在低版本使用一些动画,其实我就是用它封装的API(懒)

效果

动画1.gif

麻了,还要处理滑动到上方的问题.唉!我们程序员不能说不!!! 继续改造

private void setImageViewNearEdge(final View v) {
    Log.e("TAG", "setImageViewNearEdge: " + mLastX + "   " + mLastY);
    ViewPropertyAnimator viewPropertyAnimator = ViewPropertyAnimator.animate(this);
    //设置动画时间
    viewPropertyAnimator.setDuration(400);
    //当控件移动并且抬起手指时,判断这个控件最后的位置是偏左还是偏右
    if (mLastX < (mScreenWidth / 2)) {
        // 设置位移动画 向左移动控件位置
        viewPropertyAnimator.translationX(-v.getLeft() + 15);
    } else {
        // 设置位移动画 向右移动控件位置
        viewPropertyAnimator.translationX(mScreenWidth - v.getLeft() - v.getWidth() - 15);
    }
    /**
     * 最后移动的位置超过定义的一个位置就进行Y轴移动
     * 判断上面
     */
    Log.e("TAG", "setImageViewNearEdge: "+getPaddingTop());
    if (mLastY < getMeasuredHeight() + ImageUtil.dp2px(activity, 25)) {
    //上面加上了状态栏的一个高度
        viewPropertyAnimator.translationY(-getTop() + getMeasuredHeight()/2);
    }
    /**
     * 最后移动的位置超过定义的一个位置就进行Y轴移动
     * 判断下面
     */
    if (mLastY > mScreenHeight - getMeasuredHeight()) {
        viewPropertyAnimator.translationY(mScreenHeight - v.getTop() - v.getHeight() * 2);
    }

    viewPropertyAnimator.start();
}

动画2.gif

其实还是有点小瑕疵,因为我这个判断,判断的是你手指按下的位置,其实我觉得应该判断这个view的中心点比较好,不过我太菜了,不知道咋算,如果有大佬知道,可以评论区告诉我一下