#######IFloatingView
package com.xxx.xxxxx.view.floatingview;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.LayoutRes;
import org.jetbrains.annotations.NotNull;
public interface IFloatingView {
void remove();
ChatFloatingView show(ViewGroup container,@NonNull ViewGroup.LayoutParams layoutParams);
ChatFloatingView show(ViewGroup container,int left, int top, int right, int bottom, FloatingGravity gravity);
View getContentView();
}
#######FloatingGravity
package com.xxx.xxxx.view.floatingview;
public enum FloatingGravity {
LEFT_TOP,
LEFT_BOTTOM,
RIGHT_TOP,
RIGHT_BOTTOM
}
#######ChatFloatingView
package com.xxx.xxxx.view.floatingview;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.Window;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import androidx.annotation.IdRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.FragmentActivity;
import com.greendotcorp.conversationsdk.basicbuilt.ConversationLog;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class ChatFloatingView implements IFloatingView, FloatingLifecycle.Callback {
private static volatile ChatFloatingView mInstance;
private WeakReference<View> mFloatingView = null;
private FloatingViewListener<View> mOnFloatingViewListener = null;
private ViewGroup.LayoutParams mCachedLayoutParams = null;
private WeakReference<FragmentActivity> mTopFragmentActivityWeakReference = null;
private boolean isShowing = false;
//default activity class names as black list from 'imagePicker' library.
private final List<String> mActivityBlackList = new ArrayList<>();
private FloatingLifecycle mFloatingLifecycle = new FloatingLifecycle();
private ChatFloatingView() {
mActivityBlackList.add("ImageEntryActivity");
mActivityBlackList.add("AlbumPreviewActivity");
mActivityBlackList.add("SelectedPreviewActivity");
mActivityBlackList.add("ImagePreviewActivity");
}
public static ChatFloatingView get() {
if (mInstance == null) {
synchronized (ChatFloatingView.class) {
if (mInstance == null) {
mInstance = new ChatFloatingView();
}
}
}
return mInstance;
}
public void bindLifecycle(Application application, String[] blackActivityList) {
if (blackActivityList != null) {
for (String activityClassName : blackActivityList) {
if (!mActivityBlackList.contains(activityClassName)) {
mActivityBlackList.add(activityClassName);
}
}
}
mFloatingLifecycle.bind(application);
}
@Override
@MainThread
public void remove() {
View floatingView = getFloatingView();
if (floatingView == null) {
return;
}
floatingView.setOnClickListener(null);
ViewGroup parentView = getParentView(floatingView);
if (parentView != null) {
parentView.removeView(floatingView);
}
isShowing = false;
}
@Override
public ChatFloatingView show(ViewGroup container, @NonNull ViewGroup.LayoutParams layoutParams) {
add2ViewGroup(container, layoutParams);
return this;
}
/**
* @param container | only support [FrameLayout,ConstraintLayout,RelativeLayout]
* @return this
*/
@Override
public ChatFloatingView show(ViewGroup container, int marginLeft, int marginTop, int marginRight, int marginBottom, FloatingGravity gravity) {
ViewGroup.MarginLayoutParams marginLayoutParams = getMarginLayoutParams(container, gravity);
marginLayoutParams.setMargins(marginLeft, marginTop, marginRight, marginBottom);
add2ViewGroup(container, marginLayoutParams);
return this;
}
private ViewGroup.MarginLayoutParams getMarginLayoutParams(ViewGroup container, FloatingGravity gravity) {
if (container instanceof FrameLayout) {
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.gravity = getFrameLayoutGravity(gravity);
return layoutParams;
} else if (container instanceof ConstraintLayout) {
ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
if (gravity == FloatingGravity.LEFT_TOP) {
layoutParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
} else if (gravity == FloatingGravity.LEFT_BOTTOM) {
layoutParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
} else if (gravity == FloatingGravity.RIGHT_TOP) {
layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
} else {
layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
}
return layoutParams;
} else if (container instanceof RelativeLayout) {
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
if (gravity == FloatingGravity.LEFT_TOP) {
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_START);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
} else if (gravity == FloatingGravity.LEFT_BOTTOM) {
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_START);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
} else if (gravity == FloatingGravity.RIGHT_TOP) {
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_END);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
} else {
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_END);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
}
return layoutParams;
} else {
throw new IllegalStateException("unsupported type of" + container + " , parameter of container must be FrameLayout or ConstraintLayout or RelativeLayout]");
}
}
private int getFrameLayoutGravity(FloatingGravity gravity) {
int newGravity;
if (gravity == FloatingGravity.LEFT_TOP) {
newGravity = Gravity.START | Gravity.TOP;
} else if (gravity == FloatingGravity.LEFT_BOTTOM) {
newGravity = Gravity.START | Gravity.BOTTOM;
} else if (gravity == FloatingGravity.RIGHT_TOP) {
newGravity = Gravity.END | Gravity.TOP;
} else {
newGravity = Gravity.END | Gravity.BOTTOM;
}
return newGravity;
}
private void add2ViewGroup(ViewGroup container, ViewGroup.LayoutParams layoutParams) {
View rootView = null;
if (mOnFloatingViewListener != null) {
rootView = mOnFloatingViewListener.getLayoutView();
}
if (container == null || rootView == null || layoutParams == null) {
return;
}
mCachedLayoutParams = layoutParams;
ViewGroup parentView = getParentView(rootView);
if (parentView != null) {
parentView.removeView(rootView);
}
rootView.setLayoutParams(mCachedLayoutParams);
mFloatingLifecycle.listener(this);
rootView.setOnClickListener(onClickListener);
isShowing = true;
mFloatingView = new WeakReference<>(rootView);
container.addView(rootView);
}
private final View.OnClickListener onClickListener = v -> {
if (mOnFloatingViewListener != null) {
mOnFloatingViewListener.onClick(v);
}
};
public void performClick() {
View floatingView = getFloatingView();
if (floatingView == null) {
return;
}
if (!floatingView.hasOnClickListeners()) {
floatingView.setOnClickListener(onClickListener);
}
floatingView.performClick();
}
public void setOnViewListener(FloatingViewListener<View> clickListener) {
mOnFloatingViewListener = clickListener;
}
@Override
@Nullable
public View getContentView() {
return getFloatingView();
}
@Nullable
public ViewGroup getParentView(View previousView) {
if (previousView == null) {
return null;
}
ViewParent viewParent = previousView.getParent();
if (viewParent instanceof ViewGroup) {
return (ViewGroup) viewParent;
}
return null;
}
public <T extends View> void setSpecifiedViewVisible(@IdRes int resId, int visible) {
View currentFloatingView = getFloatingView();
if (currentFloatingView == null) {
return;
}
T view = currentFloatingView.findViewById(resId);
if (view == null) {
return;
}
view.setVisibility(visible);
}
@Nullable
private View getFloatingView() {
if (mFloatingView == null) {
return null;
}
return mFloatingView.get();
}
public boolean isFloatingVisible() {
return isShowing;
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
Activity lastTopActivity = getTopFragmentActivity();
if (lastTopActivity == activity || mActivityBlackList.contains(activity.getClass().getSimpleName())) {
return;
}
//When lastTopActivity != activity then update Top Fragment Activity.
updateTopFragmentActivity(activity);
add2ViewGroup(getTopActivityDecorView(), mCachedLayoutParams);
}
@Nullable
private ViewGroup getTopActivityDecorView() {
FragmentActivity activity = getTopFragmentActivity();
return getActivityDecorView(activity);
}
@Nullable
public ViewGroup getActivityDecorView(Activity activity) {
if (activity == null) {
return null;
}
initTopFragmentActivity(activity);
Window window = activity.getWindow();
if (window == null) {
return null;
}
View view = window.getDecorView();
if (view instanceof ViewGroup) {
return (ViewGroup) view;
}
return null;
}
private <TOP_ACTIVITY extends Activity> void initTopFragmentActivity(TOP_ACTIVITY currentActivity) {
updateTopFragmentActivity(currentActivity);
}
private <TOP_ACTIVITY extends Activity> void updateTopFragmentActivity(TOP_ACTIVITY topActivity) {
if (!(topActivity instanceof FragmentActivity)) {
ConversationLog.INSTANCE.d("set topActivity failure,because activity's type is not correct.type=" + topActivity);
return;
}
if (mTopFragmentActivityWeakReference == null) {
mTopFragmentActivityWeakReference = new WeakReference<>((FragmentActivity) topActivity);
return;
}
FragmentActivity lastFragmentActivity = mTopFragmentActivityWeakReference.get();
if (lastFragmentActivity == null) {
mTopFragmentActivityWeakReference = new WeakReference<>((FragmentActivity) topActivity);
return;
}
if (!(topActivity.getClass() == lastFragmentActivity.getClass())) {
mTopFragmentActivityWeakReference = new WeakReference<>((FragmentActivity) topActivity);
}
}
@Nullable
public FragmentActivity getTopFragmentActivity() {
if (mTopFragmentActivityWeakReference == null || mTopFragmentActivityWeakReference.get() == null) {
return null;
}
return mTopFragmentActivityWeakReference.get();
}
public void destroy() {
remove();
mOnFloatingViewListener = null;
mFloatingView = null;
mFloatingLifecycle.unbind();
mFloatingLifecycle = null;
mActivityBlackList.clear();
mCachedLayoutParams = null;
if (mTopFragmentActivityWeakReference != null) {
mTopFragmentActivityWeakReference.clear();
mTopFragmentActivityWeakReference = null;
}
mInstance = null;
}
}
#######FloatingViewListener
package com.xxx.xxxxx.view.floatingview;
public interface FloatingViewListener<T> {
@Nullable
View getLayoutView();
void onClick(T view);
void onRemove(T view);
}
#######FloatingMagnetLayout
package com.xxx.xxxx.view.floatingview;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewGroup;
import androidx.constraintlayout.widget.ConstraintLayout;
public class FloatingMagnetLayout extends ConstraintLayout {
public static final int MARGIN_EDGE = 13;
private float mOriginalRawX;
private float mOriginalRawY;
private float mOriginalX;
private float mOriginalY;
private FloatingViewListener<FloatingMagnetLayout> mFloatingViewListener;
private static final int TOUCH_TIME_THRESHOLD = 150;
private long mLastTouchDownTime;
protected MoveAnimator mMoveAnimator;
protected int mScreenWidth;
private int mScreenHeight;
private int mStatusBarHeight;
private boolean isNearestLeft = true;
private float mPortraitY;
public void setFloatingViewListener(FloatingViewListener<FloatingMagnetLayout> magnetViewListener) {
this.mFloatingViewListener = magnetViewListener;
}
public FloatingMagnetLayout(Context context) {
this(context, null);
}
public FloatingMagnetLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FloatingMagnetLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mMoveAnimator = new MoveAnimator();
// mStatusBarHeight = AppUtils.getStatusBarHeight(getContext());
mStatusBarHeight = 0;
setClickable(true);
// updateSize();
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event == null) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
changeOriginalTouchParams(event);
updateSize();
mMoveAnimator.stop();
break;
case MotionEvent.ACTION_MOVE:
updateViewPosition(event);
break;
case MotionEvent.ACTION_UP:
clearPortraitY();
moveToEdge();
if (isOnClickEvent()) {
dealClickEvent();
}
break;
}
return true;
}
protected void dealClickEvent() {
if (mFloatingViewListener != null) {
mFloatingViewListener.onClick(this);
}
}
protected boolean isOnClickEvent() {
return System.currentTimeMillis() - mLastTouchDownTime < TOUCH_TIME_THRESHOLD;
}
private void updateViewPosition(MotionEvent event) {
setX(mOriginalX + event.getRawX() - mOriginalRawX);
float desY = mOriginalY + event.getRawY() - mOriginalRawY;
if (desY < mStatusBarHeight) {
desY = mStatusBarHeight;
}
if (desY > mScreenHeight - getHeight()) {
desY = mScreenHeight - getHeight();
}
setY(desY);
}
private void changeOriginalTouchParams(MotionEvent event) {
mOriginalX = getX();
mOriginalY = getY();
mOriginalRawX = event.getRawX();
mOriginalRawY = event.getRawY();
mLastTouchDownTime = System.currentTimeMillis();
}
protected void updateSize() {
ViewGroup viewGroup = (ViewGroup) getParent();
if (viewGroup != null) {
mScreenWidth = viewGroup.getWidth() - getWidth();
mScreenHeight = viewGroup.getHeight();
}
}
public void moveToEdge() {
moveToEdge(isNearestLeft(), false);
}
public void moveToEdge(boolean isLeft, boolean isLandscape) {
float moveDistance = isLeft ? MARGIN_EDGE : mScreenWidth - MARGIN_EDGE;
float y = getY();
if (!isLandscape && mPortraitY != 0) {
y = mPortraitY;
clearPortraitY();
}
mMoveAnimator.start(moveDistance, Math.min(Math.max(0, y), mScreenHeight - getHeight()));
}
private void clearPortraitY() {
mPortraitY = 0;
}
protected boolean isNearestLeft() {
int middle = mScreenWidth / 2;
isNearestLeft = getX() < middle;
return isNearestLeft;
}
public void onRemove() {
if (mFloatingViewListener != null) {
mFloatingViewListener.onRemove(this);
}
}
protected class MoveAnimator implements Runnable {
private final 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);
}
}
private void stop() {
handler.removeCallbacks(this);
}
}
private void move(float deltaX, float deltaY) {
setX(getX() + deltaX);
setY(getY() + deltaY);
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (getParent() != null) {
final boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
markPortraitY(isLandscape);
((ViewGroup) getParent()).post(() -> {
updateSize();
moveToEdge(isNearestLeft, isLandscape);
});
}
}
private void markPortraitY(boolean isLandscape) {
if (isLandscape) {
mPortraitY = getY();
}
}
}
WindowManage 方式(使用时需要配合权限申请并需要适配机型)
package com.xxxx.xxxx.baseui.floatingview;
import static android.content.Context.WINDOW_SERVICE;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import androidx.annotation.CallSuper;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import com.greendotcorp.conversationsdk.basicbuilt.ConversationLog;
import java.lang.ref.WeakReference;
public class FloatingWindowView {
private WindowManager.LayoutParams mLayoutParams;
private WindowManager mWindowManager;
private boolean mAdded;
private WeakReference<View> mFloatingView = null;
private View.OnClickListener mOnViewClickListener = null;
private static volatile FloatingWindowView mInstance = null;
public static FloatingWindowView get() {
if (mInstance == null) {
synchronized (FloatingWindowView.class) {
if (mInstance == null) {
mInstance = new FloatingWindowView();
}
}
}
return mInstance;
}
@CallSuper
public synchronized void show(View layoutView, FloatingGravity gravity, int x, int y) {
if (layoutView == null) {
throw new NullPointerException("layoutView can not be null");
}
Context context = layoutView.getContext();
if (context == null) {
throw new NullPointerException("context can not be null");
}
View tempFloatingView = getFloatingView();
if (tempFloatingView != null && mAdded) {
tempFloatingView.setVisibility(View.VISIBLE);
return;
}
if (mWindowManager == null) {
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
}
if (tempFloatingView == null) {
mFloatingView = new WeakReference<>(layoutView);
}
View floatingView = getFloatingView();
if (mLayoutParams == null) {
mLayoutParams = new WindowManager.LayoutParams();
}
mLayoutParams.format = PixelFormat.TRANSLUCENT;
mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
floatingView.setOnClickListener(mOnViewClickListener);
floatingView.setVisibility(View.VISIBLE);
mLayoutParams.gravity = getFrameLayoutGravity(gravity);
mLayoutParams.x = x;
mLayoutParams.y = y;
try {
mWindowManager.addView(floatingView, mLayoutParams);
mAdded = true;
} catch (Exception e) {
Log.e("ExceptionTag","add floating view failure,please check.");
}
}
private int getFrameLayoutGravity(FloatingGravity gravity) {
int newGravity;
if (gravity == FloatingGravity.LEFT_TOP) {
newGravity = Gravity.START | Gravity.TOP;
} else if (gravity == FloatingGravity.LEFT_BOTTOM) {
newGravity = Gravity.START | Gravity.BOTTOM;
} else if (gravity == FloatingGravity.RIGHT_TOP) {
newGravity = Gravity.END | Gravity.TOP;
} else {
newGravity = Gravity.END | Gravity.BOTTOM;
}
return newGravity;
}
@CallSuper
public void invisible() {
View floatingView = getFloatingView();
if (floatingView != null) {
floatingView.setVisibility(View.INVISIBLE);
}
}
@CallSuper
public void gone() {
View floatingView = getFloatingView();
if (floatingView != null) {
floatingView.setVisibility(View.GONE);
}
}
@CallSuper
public void remove() {
View floatingView = getFloatingView();
if (floatingView != null && mWindowManager != null) {
if (floatingView.isAttachedToWindow()) {
mWindowManager.removeView(floatingView);
}
mAdded = false;
}
}
public void setOnViewClickListener(View.OnClickListener listener) {
mOnViewClickListener = listener;
}
public void performClick() {
View floatingView = getFloatingView();
if (mOnViewClickListener != null) {
mOnViewClickListener.onClick(floatingView);
}
}
@SuppressWarnings("unchecked")
public <T extends View> T findView(@IdRes int id) {
View floatingView = getFloatingView();
if (floatingView != null) {
return (T) floatingView.findViewById(id);
}
return null;
}
public boolean isFloatingVisible() {
View floatingView = getFloatingView();
return floatingView != null && floatingView.getVisibility() == View.VISIBLE;
}
@Nullable
private View getFloatingView() {
if (mFloatingView == null) {
return null;
}
return mFloatingView.get();
}
@CallSuper
public void destroy() {
remove();
mLayoutParams = null;
mWindowManager = null;
mAdded = false;
mFloatingView = null;
mOnViewClickListener = null;
mInstance = null;
}
}
拓展:应用内悬浮适配的一点思路 https://cloud.tencent.com/developer/article/1732965