Android模仿微信浮窗功能的效果实现

转载请注明出处,谢谢:https://blog.csdn.net/HarryWeasley/article/details/82591320

最近研究了微信悬浮窗的效果实现,写此文章记录一下,后面有我的GitHub源码地址。
老规矩,先放效果图,效果如下所示:

这里写图片描述

首先,说下项目的主要几个功能点。
1.app申请悬浮窗权限,通过WindowManager添加视图
2.一共添加三个视图,右下角两个视图,分别表示小删除视图和大删除视图,一个是真正的浮窗视图
3.webView消失动画效果实现

我的整个项目,是在这个项目https://github.com/yhaolpz/FloatWindow的基础上添加和修改的,还是要感谢之前的大神的无私奉献啊。

申请权限,该项目实现了一个工具类,对于小米手机不同的系统版本,需要专门去适配,下面来判断通过哪种方式申请权限:
FloatPhone.init()

@Override
    public void init() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
            req();
        } else if (Miui.rom()) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                req();
            } else {
                mLayoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
                Miui.req(mContext, new PermissionListener() {
                    @Override
                    public void onSuccess() {
                        mWindowManager.addView(mView, mLayoutParams);
                        if (mPermissionListener != null) {
                            mPermissionListener.onSuccess();
                        }
                    }

                    @Override
                    public void onFail() {
                        if (mPermissionListener != null) {
                            mPermissionListener.onFail();
                        }
                    }
                });
            }
        } else {
            try {
                mLayoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
                mWindowManager.addView(mView, mLayoutParams);
            } catch (Exception e) {
                mWindowManager.removeView(mView);
                LogUtil.e("TYPE_TOAST 失败");
                req();
            }
        }
    }

如果是小米手机,用以下方式申请权限

package com.yhao.floatwindow;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.view.View;
import android.view.WindowManager;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import static com.yhao.floatwindow.Rom.isIntentAvailable;

/**
 * Created by yhao on 2017/12/30.
 * https://github.com/yhaolpz
 * <p>
 * 需要清楚:一个MIUI版本对应小米各种机型,基于不同的安卓版本,但是权限设置页跟MIUI版本有关
 * 测试TYPE_TOAST类型:
 * 7.0:
 * 小米      5        MIUI8         -------------------- 失败
 * 小米   Note2       MIUI9         -------------------- 失败
 * 6.0.1
 * 小米   5                         -------------------- 失败
 * 小米   红米note3                  -------------------- 失败
 * 6.0:
 * 小米   5                         -------------------- 成功
 * 小米   红米4A      MIUI8         -------------------- 成功
 * 小米   红米Pro     MIUI7         -------------------- 成功
 * 小米   红米Note4   MIUI8         -------------------- 失败
 * <p>
 * 经过各种横向纵向测试对比,得出一个结论,就是小米对TYPE_TOAST的处理机制毫无规律可言!
 * 跟Android版本无关,跟MIUI版本无关,addView方法也不报错
 * 所以最后对小米6.0以上的适配方法是:不使用 TYPE_TOAST 类型,统一申请权限
 */

class Miui {

    private static final String miui = "ro.miui.ui.version.name";
    private static final String miui5 = "V5";
    private static final String miui6 = "V6";
    private static final String miui7 = "V7";
    private static final String miui8 = "V8";
    private static final String miui9 = "V9";
    private static List<PermissionListener> mPermissionListenerList;
    private static PermissionListener mPermissionListener;


    static boolean rom() {
        LogUtil.d(" Miui  : " + Miui.getProp());
        return Build.MANUFACTURER.equals("Xiaomi");
    }

    private static String getProp() {
        return Rom.getProp(miui);
    }

    /**
     * Android6.0以下申请权限
     */
    static void req(final Context context, PermissionListener permissionListener) {
        if (PermissionUtil.hasPermission(context)) {
            permissionListener.onSuccess();
            return;
        }
        if (mPermissionListenerList == null) {
            mPermissionListenerList = new ArrayList<>();
            mPermissionListener = new PermissionListener() {
                @Override
                public void onSuccess() {
                    for (PermissionListener listener : mPermissionListenerList) {
                        listener.onSuccess();
                    }
                    mPermissionListenerList.clear();
                }
                @Override
                public void onFail() {
                    for (PermissionListener listener : mPermissionListenerList) {
                        listener.onFail();
                    }
                    mPermissionListenerList.clear();
                }
            };
            req_(context);
        }
        mPermissionListenerList.add(permissionListener);
    }


    private static void req_(final Context context) {
        switch (getProp()) {
            case miui5:
                reqForMiui5(context);
                break;
            case miui6:
            case miui7:
                reqForMiui67(context);
                break;
            case miui8:
            case miui9:
                reqForMiui89(context);
                break;
        }
        FloatLifecycle.setResumedListener(new ResumedListener() {
            @Override
            public void onResumed() {
                if (PermissionUtil.hasPermission(context)) {
                    mPermissionListener.onSuccess();
                } else {
                    mPermissionListener.onFail();
                }
            }
        });
    }


    private static void reqForMiui5(Context context) {
        String packageName = context.getPackageName();
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        Uri uri = Uri.fromParts("package", packageName, null);
        intent.setData(uri);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (isIntentAvailable(intent, context)) {
            context.startActivity(intent);
        } else {
            LogUtil.e("intent is not available!");
        }
    }

    private static void reqForMiui67(Context context) {
        Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
        intent.setClassName("com.miui.securitycenter",
                "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
        intent.putExtra("extra_pkgname", context.getPackageName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (isIntentAvailable(intent, context)) {
            context.startActivity(intent);
        } else {
            LogUtil.e("intent is not available!");
        }
    }

    private static void reqForMiui89(Context context) {
        Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
        intent.putExtra("extra_pkgname", context.getPackageName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (isIntentAvailable(intent, context)) {
            context.startActivity(intent);
        } else {
            intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
            intent.setPackage("com.miui.securitycenter");
            intent.putExtra("extra_pkgname", context.getPackageName());
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            if (isIntentAvailable(intent, context)) {
                context.startActivity(intent);
            } else {
                LogUtil.e("intent is not available!");
            }
        }
    }


    /**
     * 有些机型在添加TYPE-TOAST类型时会自动改为TYPE_SYSTEM_ALERT,通过此方法可以屏蔽修改
     * 但是...即使成功显示出悬浮窗,移动的话也会崩溃
     */
    private static void addViewToWindow(WindowManager wm, View view, WindowManager.LayoutParams params) {
        setMiUI_International(true);
        wm.addView(view, params);
        setMiUI_International(false);
    }


    private static void setMiUI_International(boolean flag) {
        try {
            Class BuildForMi = Class.forName("miui.os.Build");
            Field isInternational = BuildForMi.getDeclaredField("IS_INTERNATIONAL_BUILD");
            isInternational.setAccessible(true);
            isInternational.setBoolean(null, flag);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}

获取到权限后,就开始添加视图了。这里主要说下,右下角的小的消除视图,因为他有个动画效果,从右下角底部移动到某个坐标点,动画实现方式如下所示:

 private void showWithAnimator(final boolean isShow) {
        if (xCancelOffset == 0) {
            IFloatWindow cancelWindow = FloatWindow.get("cancel");
            if (cancelWindow != null) {
                int[] array = cancelWindow.getOffset();
                xCancelOffset = array[0];
                yCancelOffset = array[1];
            }
        }
        if (xCoordinate == 0) {
            xCoordinate = Util.getScreenWidth(mB.mApplicationContext);
            yCoordinate = Util.getScreenHeight(mB.mApplicationContext);
        }
        ValueAnimator mAnimator = new ValueAnimator();
        mAnimator.setDuration(500);
        if (isShow) {
            mAnimator.setObjectValues(new PointF(xCoordinate, yCoordinate), new PointF(xCancelOffset, yCancelOffset));
        } else {
            mAnimator.setObjectValues(new PointF(xCancelOffset, yCancelOffset), new PointF(xCoordinate, yCoordinate));
        }

        mAnimator.setEvaluator(new TypeEvaluator<PointF>() {
            @Override
            public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
                int valueX = (int) (startValue.getX() + fraction * (endValue.getX() - startValue.getX()));
                int valueY = (int) (startValue.getY() + fraction * (endValue.getY() - startValue.getY()));
                return new PointF(valueX, valueY);
            }
        });
        mAnimator.start();
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                PointF point = (PointF) valueAnimator.getAnimatedValue();

                mFloatView.updateXY(point.getX(), point.getY());

            }
        });
    }

第二个难点是,webView的消失动画效果。

我试过很多次,webView想要实现一个圆角的渐变动画,很难实现,所以最后我选择了一个替代方法,就是先将webView的视图获取到,设置到ImageView中,然后将ImageView设置相应的动画即可:

代码如下所示:

package demo.com.lgx.wechatfloatdemo.weghit;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.Xfermode;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;

/**
 * Created by Harry on 2018/8/9.
 * desc:
 */

public class ScaleCircleImageView extends AppCompatImageView {
    private RectF mRectF;
    private ScaleCircleAnimation scaleCircleAnimation;
    private Paint mPaint;
    private ScaleCircleListener listener;
    Bitmap src;
    private Xfermode xfermode;

    public ScaleCircleImageView(Context context) {
        super(context);
        setWillNotDraw(false);
    }

    public ScaleCircleImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setWillNotDraw(false);
    }

    public ScaleCircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setWillNotDraw(false);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mPaint == null) {
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mPaint.setDither(true);
        }
        if (mRectF == null) {
            mRectF = new RectF();
        }
        if (xfermode == null) {
            xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
        }
        if (scaleCircleAnimation != null) {
            int left = scaleCircleAnimation.getLeftX();
            int top = scaleCircleAnimation.getTopY();
            int right = scaleCircleAnimation.getRightX();
            int bottom = scaleCircleAnimation.getBottomY();
            float radius = scaleCircleAnimation.getRadius();
            mRectF.set(left, top, right, bottom);
//            canvas.clipRect(mRectF);
            canvas.drawRoundRect(mRectF, radius, radius, mPaint);
            //设置Xfermode
            mPaint.setXfermode(xfermode);
            //源图
            canvas.drawBitmap(src, 0, 0, mPaint);
            //还原Xfermode
            mPaint.setXfermode(null);

        }
    }

    private int width;


    public void startAnimation(Bitmap bitmap, int width) {

        if (animationParam == null) {
            throw new IllegalArgumentException("animationParam has  been init!");
        }
        this.width = width;
        src = bitmap;
        ValueAnimator valueAnimator = new ValueAnimator();
        valueAnimator.setObjectValues(new ScaleCircleAnimation(animationParam.fromLeftX, animationParam.fromRightX, animationParam.fromTopY, animationParam.fromBottomY, animationParam.fromRadius),
                new ScaleCircleAnimation(animationParam.toLeftX, animationParam.toRightX, animationParam.toTopY, animationParam.toBottomY, animationParam.toRadius));
        valueAnimator.setEvaluator(new TypeEvaluator<ScaleCircleAnimation>() {
            @Override
            public ScaleCircleAnimation evaluate(float fraction, ScaleCircleAnimation startValue, ScaleCircleAnimation endValue) {
                int leftX = (int) (startValue.getLeftX() + fraction * (endValue.getLeftX() - startValue.getLeftX()));
                int topY = (int) (startValue.getTopY() + fraction * (endValue.getTopY() - startValue.getTopY()));
                int rightX = (int) (startValue.getRightX() + fraction * (endValue.getRightX() - startValue.getRightX()));
                int bottomY = (int) (startValue.getBottomY() + fraction * (endValue.getBottomY() - startValue.getBottomY()));
                float radius = (startValue.getRadius() + fraction * (endValue.getRadius() - startValue.getRadius()));
                return new ScaleCircleAnimation(leftX, rightX, topY, bottomY, radius);
            }
        });
        valueAnimator.setDuration(500);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                scaleCircleAnimation = (ScaleCircleAnimation) animation.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.start();
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                if (listener != null) {
                    listener.onAnimationEnd();
                }
            }
        });
    }

    private  AnimationParam animationParam;

    public  AnimationParam createAnmiationParam() {
        return animationParam = new AnimationParam();
    }


    public  class AnimationParam {
        int fromLeftX;
        int fromRightX;
        int toLeftX;
        int toRightX;
        int fromTopY;
        int fromBottomY;
        int toTopY;
        int toBottomY;
        int fromRadius;
        int toRadius;


        public AnimationParam setFromLeftX(int fromLeftX) {
            this.fromLeftX = fromLeftX;
            return this;
        }

        public AnimationParam setFromRightX(int fromRightX) {
            this.fromRightX = fromRightX;
            return this;
        }

        public AnimationParam setToLeftX(int toLeftX) {
            this.toLeftX = toLeftX;
            return this;
        }

        public AnimationParam setToRightX(int toRightX) {
            this.toRightX = toRightX;
            return this;
        }

        public AnimationParam setFromTopY(int fromTopY) {
            this.fromTopY = fromTopY;
            return this;
        }

        public AnimationParam setFromBottomY(int fromBottomY) {
            this.fromBottomY = fromBottomY;
            return this;
        }

        public AnimationParam setToTopY(int toTopY) {
            this.toTopY = toTopY;
            return this;
        }

        public AnimationParam setToBottomY(int toBottomY) {
            this.toBottomY = toBottomY;
            return this;
        }

        public AnimationParam setFromRadius(int fromRadius) {
            this.fromRadius = fromRadius;
            return this;
        }

        public AnimationParam setToRadius(int toRadius) {
            this.toRadius = toRadius;
            return this;
        }
    }

    public void setScaleCircleListener(ScaleCircleListener listener) {
        this.listener = listener;

    }

    public interface ScaleCircleListener {
        void onAnimationEnd();
    }
}

动画主要是以下的代码:

  mRectF.set(left, top, right, bottom);
  canvas.drawRoundRect(mRectF, radius, radius, mPaint);
   //设置Xfermode
  mPaint.setXfermode(xfermode);

通过PorterDuffXfermode通过以下形式,来叠放两个图片和被切的视图

  xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);

其中,图片的获取是以下方式:

  View view = parent;
        Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        view.draw(canvas);

项目中,因为项目中代码太多,还有很多内容没有在博客中写出来,如果大家有问题,可以在文末说出来,谢谢。

源码地址:https://github.com/HarryWeasley/weChatFloatDemo
参考文章:
Andorid 任意界面悬浮窗,实现悬浮窗如此简单
https://github.com/yhaolpz/FloatWindow
Android动画篇(二):颜色和形状改变的ChangeShapeAndColorButton
https://blog.csdn.net/u011315960/article/details/74984417
Android图形处理–PorterDuff.Mode那些事儿
https://blog.csdn.net/HardWorkingAnt/article/details/78045232

猜你喜欢

转载自blog.csdn.net/HarryWeasley/article/details/82591320