转载请注明出处,谢谢: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()
@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