转载请注明出处,谢谢:https://blog.csdn.net/HarryWeasley/article/details/82591320
源码地址:https://github.com/HarryWeasley/weChatFloatDemo
最近研究了微信悬浮窗的效果实现,写此文章记录一下,后面有我的GitHub源码地址。
老规矩,先放效果图,效果如下所示:
首先,说下项目的主要几个功能点。
1.app申请悬浮窗权限,通过WindowManager添加视图
2.一共添加三个视图,右下角两个视图,分别表示小删除视图和大删除视图,一个是真正的浮窗视图
3.webView消失动画效果实现
我的整个项目,是在这个项目https://github.com/yhaolpz/FloatWindow的基础上添加和修改的,还是要感谢之前的大神的无私奉献啊。
申请权限,该项目实现了一个工具类,对于小米手机不同的系统版本,需要专门去适配,下面来判断通过哪种方式申请权限:
FloatPhone.init()
@Overridepublic 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() {@Overridepublic void onSuccess() {mWindowManager.addView(mView, mLayoutParams);if (mPermissionListener != null) {mPermissionListener.onSuccess();}}@Overridepublic 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() {@Overridepublic void onSuccess() {for (PermissionListener listener : mPermissionListenerList) {listener.onSuccess();}mPermissionListenerList.clear();}@Overridepublic 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() {@Overridepublic 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>() {@Overridepublic 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() {@Overridepublic 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);}@Overrideprotected 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);//设置XfermodemPaint.setXfermode(xfermode);//源图canvas.drawBitmap(src, 0, 0, mPaint);//还原XfermodemPaint.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>() {@Overridepublic 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() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {scaleCircleAnimation = (ScaleCircleAnimation) animation.getAnimatedValue();invalidate();}});valueAnimator.start();valueAnimator.addListener(new AnimatorListenerAdapter() {@Overridepublic 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);//设置XfermodemPaint.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