動畫庫NineOldAndroids實戰自定義懸浮窗

語言: CN / TW / HK

小知識,大挑戰!本文正在參與「程序員必備小知識」創作活動

本文已參與 「掘力星計劃」 ,贏取創作大禮包,挑戰創作激勵金。

前言

我們項目中一直有一個 提現功能,在實際的效果中就是一個可以拖動的紅包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的中心點比較好,不過我太菜了,不知道咋算,如果有大佬知道,可以評論區吿訴我一下