无需一切权限,不受各种国产ROM限制,默认可以显示的应用内悬浮窗。
- 应用内显示,无需申请任何权限
- 应用内显示,所有机型都可以默认显示悬浮窗,无需引导用户做更多设置
- 支持拖拽
- 超出屏幕限制移动
- 可自动吸附到屏幕边缘
- 可向左右边缘隐藏一半
核心类共三个:
CFloatingManager.java
import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.view.ViewCompat;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import com.imuxuan.floatingview.utils.EnContext;
/**
* @ClassName CFloatingManager
* @Description 悬浮窗管理器, 建造者模式
*/
public class CFloatingManager {
public static FloatingImp build() {
return new FloatingImp();
}
public static class FloatingImp {
private Handler mHandler;
private CFloatingView mCFloatingView;
private FrameLayout mContainer;
int layoutId = 0; //布局id
ViewGroup.LayoutParams params; //布局初始参数(位置,大小等)
CFloatingView.MagnetViewListener magnetViewListener;//监听
CFloatingView.IFloatingViews iFloatingViews;//监听
boolean isMovable = true;//是否可移动
boolean isHideEdge = true;//是否隐藏边缘
public FloatingImp create() {
mHandler = new Handler(Looper.getMainLooper());
synchronized (this) {
if (mCFloatingView != null) {
return this;
}
mCFloatingView = new CFloatingView(EnContext.get().getApplicationContext());
mCFloatingView.setLayout(EnContext.get().getApplicationContext(), layoutId);
if (null != iFloatingViews) {
iFloatingViews.onInitViews(mCFloatingView);
}
mCFloatingView.setLayoutParams(null == params ? defaultParams() : params);
mCFloatingView.setIsMovable(isMovable);
mCFloatingView.setIsHideEdge(isHideEdge);
if (mCFloatingView != null) {
mCFloatingView.setMagnetViewListener(magnetViewListener);
}
addViewToWindow(mCFloatingView);
}
return this;
}
/**
* 设置布局控件
*/
public FloatingImp setLayout(int layoutId) {
this.layoutId = layoutId;
return this;
}
/**
* 设置布局初始参数
*/
public FloatingImp setLayoutParams(ViewGroup.LayoutParams params) {
this.params = params;
return this;
}
/**
* 监听事件
*/
public FloatingImp setListener(CFloatingView.MagnetViewListener magnetViewListener) {
this.magnetViewListener = magnetViewListener;
return this;
}
public FloatingImp setIsMovable(boolean isMovable) {
this.isMovable = isMovable;
return this;
}
public FloatingImp setIsHideEdge(boolean isHideEdge) {
this.isHideEdge = isHideEdge;
return this;
}
public FloatingImp setInitViews(CFloatingView.IFloatingViews iFloatingViews) {
this.iFloatingViews = iFloatingViews;
return this;
}
public FloatingImp remove() {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (mCFloatingView == null) {
return;
}
if (ViewCompat.isAttachedToWindow(mCFloatingView) && mContainer != null) {
mContainer.removeView(mCFloatingView);
}
mCFloatingView = null;
}
});
return this;
}
/**
* 将view绑定到activity的布局中
*/
public FloatingImp attach(Activity activity) {
attach(getActivityRoot(activity));
return this;
}
/**
* 将view绑定到布局中
*/
public FloatingImp attach(FrameLayout container) {
if (container == null || mCFloatingView == null) {
mContainer = container;
return this;
}
if (mCFloatingView.getParent() == container) {
return this;
}
if (mContainer != null && mCFloatingView.getParent() == mContainer) {
mContainer.removeView(mCFloatingView);
}
mContainer = container;
container.addView(mCFloatingView);
mCFloatingView.onAttach();
return this;
}
/**
* 将view从activity的布局中解绑
*/
public FloatingImp detach(Activity activity) {
detach(getActivityRoot(activity));
return this;
}
/**
* 将view从布局中解绑
*/
public FloatingImp detach(FrameLayout container) {
if (mCFloatingView != null && container != null && ViewCompat.isAttachedToWindow(mCFloatingView)) {
container.removeView(mCFloatingView);
}
if (mContainer == container) {
mContainer = null;
}
mCFloatingView.onDetach();
return this;
}
/**
* 将view添加到当前窗口中
*/
private void addViewToWindow(final CFloatingView view) {
if (mContainer == null) {
return;
}
mContainer.addView(view);
}
/**
* 默認Params,用于设置最开始位置
*/
private FrameLayout.LayoutParams defaultParams() {
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.BOTTOM | Gravity.START;
params.setMargins(13, params.topMargin, params.rightMargin, 56);
return params;
}
/**
* 获取activity所绑定的布局
*/
private FrameLayout getActivityRoot(Activity activity) {
if (activity == null) {
return null;
}
try {
return (FrameLayout) activity.getWindow().getDecorView().findViewById(android.R.id.content);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
}
CFloatingView.java
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.MotionEvent;
import android.view.View;
/**
* @ClassName CFloatingView
* @Description 悬浮窗,继承磁力吸附悬浮窗,封装点击事件
*/
public class CFloatingView extends FloatingMagnetView {
private MagnetViewListener mMagnetViewListener;
private long mLastTouchDownTime;//用于判断点击
private static final int TOUCH_TIME_THRESHOLD = 150;
public View view;
/**
* 悬浮窗点击接口
*/
public interface MagnetViewListener {
void onRemove(CFloatingView cFloatingView);
void onClick(CFloatingView cFloatingView);
void onEndAppear(CFloatingView cFloatingView);
void onEndHide(CFloatingView cFloatingView);
}
public interface IFloatingViews {
void onInitViews(CFloatingView cFloatingView);
}
public CFloatingView(@NonNull Context context) {
super(context, null);
}
public View setLayout(@NonNull Context context, int en_floating_view_id) {
return view = inflate(context, en_floating_view_id, this);
}
public void setMagnetViewListener(MagnetViewListener magnetViewListener) {
this.mMagnetViewListener = magnetViewListener;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
if (event != null) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastTouchDownTime = System.currentTimeMillis();
clearAnimation();
break;
case MotionEvent.ACTION_UP:
if (isOnClickEvent()) {
dealClickEvent();
}
break;
}
}
return true;
}
protected void dealClickEvent() {
if (mMagnetViewListener != null) {
mMagnetViewListener.onClick(this);
}
}
public void onRemove() {
if (mMagnetViewListener != null) {
mMagnetViewListener.onRemove(this);
}
}
protected boolean isOnClickEvent() {
return System.currentTimeMillis() - mLastTouchDownTime < TOUCH_TIME_THRESHOLD;
}
/**
* 隐藏,点击又出现之后,由子类进行具体实现
*/
@Override
protected void onEndAppear() {
super.onEndAppear();
if (mMagnetViewListener != null) {
mMagnetViewListener.onEndAppear(this);
}
}
/**
* 隐藏半边之后
*/
@Override
protected void onEndHide() {
super.onEndHide();
if (mMagnetViewListener != null) {
mMagnetViewListener.onEndHide(this);
}
}
@Override
public void onAttach() {
super.onAttach();
handler.removeCallbacksAndMessages(null);
handler.postDelayed(new Runnable() {
@Override
public void run() {
hideEdgeAnima();
}
}, 2000);
}
}
FloatingMagnetView.java
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import com.imuxuan.floatingview.utils.SystemUtils;
/**
* @ClassName FloatingMagnetView
* @Description 磁力吸附悬浮窗
* @Author Yunpeng Li
* @Creation 2018/3/15 下午5:02
* @Mender Yunpeng Li
* @Modification 2018/3/15 下午5:02
*/
public class FloatingMagnetView extends FrameLayout {
public static final int MARGIN_EDGE = 13;
private float mOriginalRawX;
private float mOriginalRawY;
private float mOriginalX;
private float mOriginalY;
protected MoveAnimator mMoveAnimator;
protected int mScreenWidth;
private int mScreenHeight;
private int mStatusBarHeight;
boolean isMovable = true;//是否可移动
public FloatingMagnetView(Context context) {
this(context, null);
}
public FloatingMagnetView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FloatingMagnetView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mMoveAnimator = new MoveAnimator();
mStatusBarHeight = SystemUtils.getStatusBarHeight(getContext());
setClickable(true);
updateSize();
}
/**
* 设置是否可移动
*/
public void setIsMovable(boolean isMovable) {
this.isMovable = isMovable;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event == null) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
changeOriginalTouchParams(event);
updateSize();
mMoveAnimator.stop();
removeRunable();
onEndAppear();
break;
case MotionEvent.ACTION_MOVE:
if (isMovable) {
updateViewPosition(event);
}
break;
case MotionEvent.ACTION_UP:
moveToEdge();
break;
}
return true;
}
/**
* 更新view的坐标位置
*/
private void updateViewPosition(MotionEvent event) {
setX(mOriginalX + event.getRawX() - mOriginalRawX);
// 限制不可超出屏幕高度
float desY = mOriginalY + event.getRawY() - mOriginalRawY;
if (desY < 0) {
desY = 0;
}
if (desY > mScreenHeight - getHeight() * 2) {
desY = mScreenHeight - getHeight() * 2;
}
setY(desY);
}
private void changeOriginalTouchParams(MotionEvent event) {
mOriginalX = getX();
mOriginalY = getY();
mOriginalRawX = event.getRawX();
mOriginalRawY = event.getRawY();
}
protected void updateSize() {
mScreenWidth = (SystemUtils.getScreenWidth(getContext()) - this.getWidth());
mScreenHeight = SystemUtils.getScreenHeight(getContext());
}
public void moveToEdge() {
float moveDistance = isNearestLeft() ? MARGIN_EDGE : mScreenWidth - MARGIN_EDGE;
mMoveAnimator.start(moveDistance, getY());
}
protected boolean isNearestLeft() {
int middle = mScreenWidth / 2;
return getX() < middle;
}
protected class MoveAnimator implements Runnable {
private Handler handler = new Handler(Looper.getMainLooper());
private float destinationX;
private float destinationY;
private long startingTime;
void start(float x, float y) {
this.destinationX = x;
this.destinationY = y;
startingTime = System.currentTimeMillis();
handler.post(this);
}
@Override
public void run() {
if (getRootView() == null || getRootView().getParent() == null) {
return;
}
float progress = Math.min(1, (System.currentTimeMillis() - startingTime) / 400f);
float deltaX = (destinationX - getX()) * progress;
float deltaY = (destinationY - getY()) * progress;
move(deltaX, deltaY);
if (progress < 1) {
handler.post(this);
} else {
hideEdgeAnima();//开始隐藏
}
}
private void stop() {
handler.removeCallbacks(this);
}
}
private void move(float deltaX, float deltaY) {
setX(getX() + deltaX);
setY(getY() + deltaY);
}
public void onAttach() {
}
public void onDetach() {
}
//关于靠边隐藏==============================================================================================
boolean isHideEdge = true;//是否隐藏边缘
protected static final int HEDE_FLOAT_VIEW_TIME = 3000;//靠边隐藏时间
protected static final int MES_ANIMA_LEFT = 0;
protected static final int MES_ANIMA_RIGHT = 1;
protected static final int VIEW_GONE = 2;
protected static final int VIEW_Transparent = 3;
protected AnimationSet animationleft;
protected AnimationSet animationright;
/**
* 设置是否隐藏边缘
*/
public void setIsHideEdge(boolean isHideEdge) {
this.isHideEdge = isHideEdge;
}
/**
* 隐藏边缘动画
*/
protected void hideEdgeAnima() {
if (isHideEdge) {
if (isNearestLeft()) {
handler.postDelayed(myRunnableLeft, HEDE_FLOAT_VIEW_TIME);
} else {
handler.postDelayed(myRunnableRigth, HEDE_FLOAT_VIEW_TIME);
}
}
}
private void removeRunable() {
handler.removeCallbacksAndMessages(null);
}
protected Runnable runnableViewGone = new Runnable() {
public void run() {
Message message = handler.obtainMessage();
message.what = VIEW_GONE;
handler.sendMessage(message);
}
};
protected Runnable runnableTransparent = new Runnable() {
@Override
public void run() {
Message message = handler.obtainMessage();
message.what = VIEW_Transparent;
handler.sendMessage(message);
}
};
protected Runnable myRunnableLeft = new Runnable() {
public void run() {
Message message = handler.obtainMessage();
message.what = MES_ANIMA_LEFT;
handler.sendMessage(message);
}
};
protected Runnable myRunnableRigth = new Runnable() {
public void run() {
Message message = handler.obtainMessage();
message.what = MES_ANIMA_RIGHT;
handler.sendMessage(message);
}
};
protected Handler handler = new Handler(Looper.getMainLooper()) {
public void handleMessage(Message msg) {
switch (msg.what) {
case MES_ANIMA_LEFT:
if (null == animationleft) {
animationleft = new AnimationSet(true);
animationleft.setDuration(250);
animationleft.addAnimation(new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, -0.5f, 0, 0, 0, 0));
animationleft.setFillAfter(true);
}
startAnimation(animationleft);
animationleft.setAnimationListener(ainimaLeft);
break;
case MES_ANIMA_RIGHT:
if (null == animationright) {
animationright = new AnimationSet(true);
animationright.setDuration(250);
animationright.addAnimation(new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0.5f, 0, 0, 0, 0));
animationright.setFillAfter(true);
}
startAnimation(animationright);
animationright.setAnimationListener(ainimaRigth);
break;
// case VIEW_GONE:
//
// imgFloatView.setVisibility(View.GONE);
// Log.d(TAG, "handleMessage: bingo");
// break;
case VIEW_Transparent:
break;
default:
break;
}
}
};
protected Animation.AnimationListener ainimaLeft = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
onEndHide();
}
};
protected Animation.AnimationListener ainimaRigth = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
onEndHide();
}
};
/**
* 隐藏,点击又出现之后,由子类进行具体实现
*/
protected void onEndAppear() {
}
/**
* 隐藏半边之后,由子类进行具体实现
*/
protected void onEndHide() {
}
}
两个个辅助工具类:
EnContext.java
import android.app.Application;
/**
* Created by Yunpeng Li on 2018/11/8.
*/
public class EnContext {
private static final Application INSTANCE;
static {
Application app = null;
try {
app = (Application) Class.forName("android.app.AppGlobals").getMethod("getInitialApplication").invoke(null);
if (app == null)
throw new IllegalStateException("Static initialization of Applications must be on main thread.");
} catch (final Exception e) {
e.printStackTrace();
try {
app = (Application) Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null);
} catch (final Exception ex) {
e.printStackTrace();
}
} finally {
INSTANCE = app;
}
}
public static Application get() {
return INSTANCE;
}
}
SystemUtils.java
import android.content.Context;
/**
* Created by Yunpeng Li on 2018/3/15.
*/
public class SystemUtils {
public static int getStatusBarHeight(Context context) {
int result = 0;
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
public static int getScreenWidth(Context context) {
int screenWith = -1;
try {
screenWith = context.getResources().getDisplayMetrics().widthPixels;
} catch (Exception e) {
e.printStackTrace();
}
return screenWith;
}
public static int getScreenHeight(Context context) {
int screenHeight = -1;
try {
screenHeight = context.getResources().getDisplayMetrics().heightPixels;
} catch (Exception e) {
e.printStackTrace();
}
return screenHeight;
}
}
应用例子:
在基础类的地方创建一个全局的CFloatingManager.FloatingImp floatingImp对象,目的是为了保持对象唯一,然后在各个子类中进行引用:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
if (null == floatingImp) {
floatingImp = CFloatingManager.build()
.setLayout(R.layout.en_floating_view)//设置布局
.setInitViews(new CFloatingView.IFloatingViews() { // 可以在此处对布局内的子控件单独进行控制,todo:设置点击事件,则会影响到拖动事件
@Override
public void onInitViews(CFloatingView cFloatingView) {
cFloatingView.view.findViewById(R.id.icon).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(TestActivity.this, "点击了icon", Toast.LENGTH_SHORT).show();
}
});
}
})
.setListener(new CFloatingView.MagnetViewListener() {//设置监听事件
@Override
public void onRemove(CFloatingView cFloatingView) {
Toast.makeText(TestActivity.this, "我没了", Toast.LENGTH_SHORT).show();
}
@Override
public void onClick(CFloatingView cFloatingView) {
Toast.makeText(TestActivity.this, "点到我了", Toast.LENGTH_SHORT).show();
}
@Override
public void onEndAppear(CFloatingView cFloatingView) {
((ImageView)cFloatingView.view.findViewById(R.id.icon)).setImageResource(R.drawable.jy_sdk_float_window_normal);
Toast.makeText(TestActivity.this, "又出现了", Toast.LENGTH_SHORT).show();
}
@Override
public void onEndHide(CFloatingView cFloatingView) {
((ImageView)cFloatingView.view.findViewById(R.id.icon)).setImageResource(R.drawable.jy_sdk_float_window_transparent);
Toast.makeText(TestActivity.this, "隐藏了一半", Toast.LENGTH_SHORT).show();
}
})
.setIsMovable(true)//控制是否可移动
.setIsHideEdge(true)//控制是否需要隐藏
.create();//创建实体
}
}
@Override
protected void onStart() {
super.onStart();
floatingImp.attach(this);//绑定实体至页面
}
@Override
protected void onStop() {
super.onStop();
floatingImp.detach(this);//移除实体离开页面
}
en_floating_view.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/icon"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/imuxuan" />
<ImageView
android:id="@+id/icon2"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_toRightOf="@+id/icon"
android:src="@drawable/imuxuan" />
</RelativeLayout>
其中的小图标我就不上了,更换一下就行