现有一款IM聊天需求,在通话页面点击缩小视图或者Home键返回桌面,点击悬浮窗回到通话页面这样一个需求。
权限
首先是权限的获取,请注意,在Android 8.0及以上版本中,需要申请悬浮窗权限(SYSTEM_ALERT_WINDOW)才能显示悬浮窗。你可以在应用启动时请求该权限,或者引导用户手动开启该权限。
后面两个权限是按返回键返回上个页面不杀死该通话页面用的任务栈用的
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.REORDER_TASKS"/>
<activity android:name="com.example.myapplication.ui.CallActivity" android:launchMode="singleInstance" android:exported="true" >
下面是悬浮窗的的CallFloatWindow类,这个类是悬浮窗的实现方法,因为我的项目有视频和语音通话功能,所有这个类的东西比较多,如果不需要这些杂七杂八的东西,看下show()方法,把计时和显示视频View相关的东西去了,就可以直接通过CallFloatWindow.getInstance().show()方法使用,这是最基本的使用方式,看你的场景需求
package com.example.myapplication.widget;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.os.Build;
import android.os.SystemClock;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.example.myapplication.MyAppLication;
import com.example.myapplication.R;
import com.example.myapplication.ui.CallActivity;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
public class CallFloatWindow {
private static final String TAG = "EaseCallFloatWindow";
private static CallFloatWindow instance;
private WindowManager windowManager = null;
private WindowManager.LayoutParams layoutParams = null;
private EaseCallMemberView memberView;
private SurfaceView surfaceView;
private View floatView;
private TextView tvContent;
private int screenWidth;
private int floatViewWidth;
private int callType; // 0语音 1视频
private int uId;
private long costSeconds;
private ConferenceInfo conferenceInfo;
private SingleCallInfo singleCallInfo;
// 计时器
private long mNow; // the currently displayed time
private long mBase;
private boolean callState = true; //是否连接成功通话
private StringBuilder mRecycle = new StringBuilder("MM:SS");
Timer timer ;
public CallFloatWindow(Context context) {
initFloatWindow(context);
}
private CallFloatWindow() {
}
public static CallFloatWindow getInstance(Context context) {
if (instance == null) {
instance = new CallFloatWindow(context);
}
return instance;
}
public static CallFloatWindow getInstance() {
if (instance == null) {
synchronized (CallFloatWindow.class) {
if (instance == null) {
instance = new CallFloatWindow();
}
}
}
return instance;
}
private void initFloatWindow(Context context) {
windowManager = (WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
Point point = new Point();
windowManager.getDefaultDisplay().getSize(point);
screenWidth = point.x;
}
private MyChronometer chronometer;
public void setCostSeconds(long seconds) {
this.costSeconds = seconds;
}
/**
* add float window
*/
public void show() { // 0: voice call; 1: video call;
if (floatView != null) {
return;
}
layoutParams = new WindowManager.LayoutParams();
layoutParams.gravity = Gravity.END | Gravity.TOP;
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.format = PixelFormat.TRANSPARENT;
layoutParams.type = getSupportedWindowType();
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
//显示的位置
layoutParams.y = 300;
floatView = LayoutInflater.from(MyAppLication.context).inflate(R.layout.activity_float_window, null);
tvContent = (TextView) floatView.findViewById(R.id.tv_content);
floatView.setFocusableInTouchMode(true);
if (floatView instanceof ViewGroup) {
chronometer = new MyChronometer(MyAppLication.context);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(150, 150);
((ViewGroup) floatView).addView(chronometer, params);
}
windowManager.addView(floatView, layoutParams);
startCount();
if (callType == 1) {
conferenceInfo = new ConferenceInfo();
} else {
singleCallInfo = new SingleCallInfo();
}
floatView.post(new Runnable() {
@Override
public void run() {
// Get the size of floatView;
if (floatView != null) {
floatViewWidth = floatView.getWidth();
}
}
});
floatView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Class<? extends Activity> callClass = CallActivity.class;
Log.e("TAG", "current call class: "+callClass);
if(callClass != null) {
Intent intent = new Intent(MyAppLication.context, callClass);
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
// if(callType != EaseCallType.CONFERENCE_CALL) {
// intent.putExtra("uId", singleCallInfo != null ? singleCallInfo.remoteUid : 0);
// }
// intent.putExtra("isClickByFloat", true);
// EaseCallKit.getInstance().getAppContext().startActivity(intent);
MyAppLication.context.startActivity(intent);
}else {
Log.e(TAG, "Current call class is null, please not call EaseCallKit.getInstance().releaseCall() before the call is finished");
}
//dismiss();
}
});
floatView.setOnTouchListener(new View.OnTouchListener() {
boolean result = false;
int left;
int top;
float startX = 0;
float startY = 0;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
result = false;
startX = event.getRawX();
startY = event.getRawY();
left = layoutParams.x;
top = layoutParams.y;
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(event.getRawX() - startX) > 20 || Math.abs(event.getRawY() - startY) > 20) {
result = true;
}
int deltaX = (int) (startX - event.getRawX());
layoutParams.x = left + deltaX;
layoutParams.y = (int) (top + event.getRawY() - startY);
windowManager.updateViewLayout(floatView, layoutParams);
break;
case MotionEvent.ACTION_UP:
smoothScrollToBorder();
break;
}
return result;
}
});
initTimeObserver();
}
public int getSupportedWindowType() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
return WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
}
private void startCount() {
if (chronometer != null) {
chronometer.setBase(SystemClock.elapsedRealtime());
chronometer.start();
}
}
private void stopCount() {
if (chronometer != null) {
chronometer.stop();
}
}
/**
* Should call the method before call {@link #dismiss()}
*
* @return Cost seconds in float window
*/
public long getFloatCostSeconds() {
if (chronometer != null) {
return chronometer.getCostSeconds();
}
Log.e(TAG, "chronometer is null, can not get cost seconds");
return 0;
}
/**
* Should call the method before call {@link #dismiss()}
*
* @return Total cost seconds
*/
public long getTotalCostSeconds() {
if (chronometer != null) {
Log.e("activity", "costSeconds: " + chronometer.getCostSeconds());
}
if (chronometer != null) {
return costSeconds + chronometer.getCostSeconds();
}
Log.e(TAG, "chronometer is null, can not get total cost seconds");
return 0;
}
public void setConferenceInfo(ConferenceInfo info) {
this.conferenceInfo = info;
}
public ConferenceInfo getConferenceInfo() {
return conferenceInfo;
}
/**
* Update conference call state
*
* @param view
*/
public void update(EaseCallMemberView view) {
if (floatView == null) {
return;
}
memberView = view;
// uId = memberView.getUserId();
if (memberView.isVideoOff()) { // 视频未开启
floatView.findViewById(R.id.layout_call_voice).setVisibility(View.VISIBLE);
floatView.findViewById(R.id.layout_call_video).setVisibility(View.GONE);
} else { // 视频已开启
floatView.findViewById(R.id.layout_call_voice).setVisibility(View.GONE);
floatView.findViewById(R.id.layout_call_video).setVisibility(View.VISIBLE);
// int uId = memberView.getUserId();
// boolean isSelf = TextUtils.equals(userAccount, EMClient.getInstance().getCurrentUser());
prepareSurfaceView(false, uId);
}
}
/**
* Update the sing call state
*
* @param isSelf
* @param curUid
* @param remoteUid
* @param surface
*/
public void update(boolean isSelf, int curUid, int remoteUid, boolean surface) {
if (singleCallInfo == null) {
singleCallInfo = new SingleCallInfo();
}
singleCallInfo.curUid = curUid;
singleCallInfo.remoteUid = remoteUid;
if (callType == 1 && surface) {
floatView.findViewById(R.id.layout_call_voice).setVisibility(View.GONE);
floatView.findViewById(R.id.layout_call_video).setVisibility(View.VISIBLE);
prepareSurfaceView(isSelf, isSelf ? curUid : remoteUid);
} else {
floatView.findViewById(R.id.layout_call_voice).setVisibility(View.VISIBLE);
floatView.findViewById(R.id.layout_call_video).setVisibility(View.GONE);
}
}
public void setContent(String text) {
if (tvContent != null) {
tvContent.post(new Runnable() {
@Override
public void run() {
tvContent.setText(text);
}
});
}
}
public SingleCallInfo getSingleCallInfo() {
return singleCallInfo;
}
public void setCameraDirection(boolean isFront, boolean changeFlag) {
if (singleCallInfo == null) {
singleCallInfo = new SingleCallInfo();
}
singleCallInfo.isCameraFront = isFront;
singleCallInfo.changeFlag = changeFlag;
}
public boolean isShowing() {
if (callType == 1) {
return memberView != null;
} else {
return floatView != null;
}
}
/**
* For the single call, only the remote uid is returned
*
* @return
*/
public int getUid() {
/* if(callType == EaseCallType.CONFERENCE_CALL && memberView != null) {
return memberView.getUserId();
}else if((callType == EaseCallType.SINGLE_VIDEO_CALL || callType == EaseCallType.SINGLE_VOICE_CALL) && singleCallInfo != null) {
return singleCallInfo.remoteUid;
}*/
return -1;
}
/**
* 停止悬浮窗
*/
public void dismiss() {
Log.i(TAG, "dismiss: ");
if (windowManager != null && floatView != null) {
stopCount();
windowManager.removeView(floatView);
}
cancel();
floatView = null;
memberView = null;
surfaceView = null;
if (conferenceInfo != null) {
conferenceInfo = null;
}
if (singleCallInfo != null) {
singleCallInfo = null;
}
}
/**
* 设置视频
*/
private void prepareSurfaceView(boolean isSelf, int uid) {
RelativeLayout surfaceLayout = (RelativeLayout) floatView.findViewById(R.id.layout_call_video);
surfaceLayout.removeAllViews();
// surfaceView =pocEngine.getRendererView(IPocEngineEventHandler.SurfaceType.LOCAL_SURFACE_TEXTURE);
surfaceLayout.addView(surfaceView);
surfaceView.setZOrderOnTop(false);
surfaceView.setZOrderMediaOverlay(false);
/* if(isSelf){
rtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN,0));
}else{
rtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, uid));
}*/
}
private void smoothScrollToBorder() {
Log.i(TAG, "screenWidth: " + screenWidth + ", floatViewWidth: " + floatViewWidth);
int splitLine = screenWidth / 2 - floatViewWidth / 2;
final int left = layoutParams.x;
final int top = layoutParams.y;
int targetX;
if (left < splitLine) {
// 滑动到最左边
targetX = 0;
} else {
// 滑动到最右边
targetX = screenWidth - floatViewWidth;
}
ValueAnimator animator = ValueAnimator.ofInt(left, targetX);
animator.setDuration(100)
.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (floatView == null) return;
int value = (int) animation.getAnimatedValue();
Log.i(TAG, "onAnimationUpdate, value: " + value);
layoutParams.x = value;
layoutParams.y = top;
windowManager.updateViewLayout(floatView, layoutParams);
}
});
animator.start();
}
public static class SingleCallInfo {
/**
* Current user's uid
*/
public int curUid;
/**
* The other size of uid
*/
public int remoteUid;
/**
* Camera direction: front or back
*/
public boolean isCameraFront = true;
/**
* A tag used to mark the switch between local and remote video
*/
public boolean changeFlag;
}
/**
* Use to hold the conference info
*/
public static class ConferenceInfo {
public Map<Integer, ViewState> uidToViewList;
public Map<String, Integer> userAccountToUidMap;
// public Map<Integer, EaseUserAccount> uidToUserAccountMap;
/**
* Hold the states of {@link EaseCallMemberView}
*/
public static class ViewState {
// video state
public boolean isVideoOff;
// audio state
public boolean isAudioOff;
// screen mode
public boolean isFullScreenMode;
// speak activate state
public boolean speakActivated;
// camera direction
public boolean isCameraFront;
}
}
public long getmBase() {
return mBase;
}
public void setmBase(long mBase) {
this.mBase = mBase;
}
public boolean isCallState() {
return callState;
}
public void setCallState(boolean callState) {
this.callState = callState;
}
public void initTimeObserver() {
timer = new Timer();
//双重保证通话中
if (callState) {
timer.schedule(new TimerTask() {
@Override
public void run() {
updateText(SystemClock.elapsedRealtime());
}
}, 0, 1000);
/* Observable.timer(1000, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.newThread())
.doOnSubscribe(disposable -> disposableObserver=disposable)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long aLong) throws Exception {
updateText(SystemClock.elapsedRealtime());
}
});*/
} else {
setContent("等待接听");
}
}
public void cancel() {
//timer cancel后不能再次调用schedule方法,需要重新创建,所以可以调用task.cancel方法取消任务
//timer.cancel();
if (timer != null) {
timer.cancel();
}
}
private synchronized void updateText(long now) {
mNow = now;
long seconds = now - mBase;
seconds /= 1000;
boolean negative = false;
if (seconds < 0) {
seconds = -seconds;
negative = true;
}
String text = DateUtils.formatElapsedTime(mRecycle, seconds);
setContent(text);
}
}
activity_float_window布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="88dp"
android:layout_height="98dp"
android:background="#D68888"
android:padding="20dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/layout_call_voice"
android:layout_width="100dp"
android:layout_height="100dp"
android:gravity="center"
android:orientation="vertical"
android:padding="20dip"
android:background="#d8d8d8"
>
<ImageView
android:id="@+id/iv_typer"
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@drawable/ic_launcher_foreground" />
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="12sp"
android:text=""
android:textStyle="bold"
android:layout_marginTop="6dp"
/>
</LinearLayout>
<!--显示视频窗口-->
<RelativeLayout
android:id="@+id/layout_call_video"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/transparent"
android:gravity="center"
android:orientation="vertical"
android:padding="20dip"
android:visibility="gone" />
</RelativeLayout>
上面说了有计时器,这个计时器我是加了个自定义的MyChronometer类来管理计时器
<string name="negative_duration">\u2212<xliff:g id="time" example="1:14">%1$s</xliff:g></string>
attrs.xml里加个自定义的属性
<declare-styleable name="MyChronometer"> <!-- Format string: if specified, the Chronometer will display this string, with the first "%s" replaced by the current timer value in "MM:SS" or "H:MM:SS" form. If no format string is specified, the Chronometer will simply display "MM:SS" or "H:MM:SS". --> <attr name="format" format="string" localization="suggested" /> <!-- Specifies whether this Chronometer counts down or counts up from the base. If not specified this is false and the Chronometer counts up. --> <attr name="countDown" format="boolean" /> </declare-styleable>
package com.example.myapplication.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.icu.text.MeasureFormat;
import android.icu.util.Measure;
import android.icu.util.MeasureUnit;
import android.net.Uri;
import android.os.Build;
import android.os.SystemClock;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.Chronometer;
import androidx.annotation.InspectableProperty;
import com.example.myapplication.R;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.IllegalFormatException;
import java.util.Locale;
public class MyChronometer extends androidx.appcompat.widget.AppCompatTextView {
private static final String TAG = "Chronometer";
/**
* A callback that notifies when the chronometer has incremented on its own.
*/
public interface OnChronometerTickListener {
/**
* Notification that the chronometer has changed.
*/
void onChronometerTick(MyChronometer chronometer);
}
private long mBase;
private long mNow; // the currently displayed time
private boolean mVisible;
private boolean mStarted;
private boolean mRunning;
private boolean mLogged;
private String mFormat;
private Formatter mFormatter;
private Locale mFormatterLocale;
private Object[] mFormatterArgs = new Object[1];
private StringBuilder mFormatBuilder;
private OnChronometerTickListener mOnChronometerTickListener;
private StringBuilder mRecycle = new StringBuilder(8);
private boolean mCountDown;
private long costSeconds;
/**
* Initialize this Chronometer object.
* Sets the base to the current time.
*/
public MyChronometer(Context context) {
this(context, null, 0);
}
/**
* Initialize with standard view layout information.
* Sets the base to the current time.
*/
public MyChronometer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* Initialize with standard view layout information and style.
* Sets the base to the current time.
*/
public MyChronometer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.MyChronometer, defStyleAttr, 0);
// saveAttributeDataForStyleable(context, R.styleable.MyChronometer,
// attrs, a, defStyleAttr, 0);
setFormat(a.getString(R.styleable.MyChronometer_format));
setCountDown(a.getBoolean(R.styleable.MyChronometer_countDown, false));
a.recycle();
init();
}
private void init() {
mBase = SystemClock.elapsedRealtime();
updateText(mBase);
}
/**
* Set this view to count down to the base instead of counting up from it.
*
* @param countDown whether this view should count down
*
* @see #setBase(long)
*/
public void setCountDown(boolean countDown) {
mCountDown = countDown;
updateText(SystemClock.elapsedRealtime());
}
/**
* @return whether this view counts down
*
* @see #setCountDown(boolean)
*/
@InspectableProperty
public boolean isCountDown() {
return mCountDown;
}
/**
* @return whether this is the final countdown
*/
public boolean isTheFinalCountDown() {
try {
getContext().startActivity(
new Intent(Intent.ACTION_VIEW, Uri.parse("https://youtu.be/9jK-NcRmVcw"))
.addCategory(Intent.CATEGORY_BROWSABLE)
.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT
| Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT));
return true;
} catch (Exception e) {
return false;
}
}
/**
* Set the time that the count-up timer is in reference to.
*
* @param base Use the {@link SystemClock#elapsedRealtime} time base.
*/
public void setBase(long base) {
mBase = base;
dispatchChronometerTick();
updateText(SystemClock.elapsedRealtime());
}
/**
* Return the base time as set through {@link #setBase}.
*/
public long getBase() {
return mBase;
}
/**
* Sets the format string used for display. The Chronometer will display
* this string, with the first "%s" replaced by the current timer value in
* "MM:SS" or "H:MM:SS" form.
*
* If the format string is null, or if you never call setFormat(), the
* Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS"
* form.
*
* @param format the format string.
*/
public void setFormat(String format) {
mFormat = format;
if (format != null && mFormatBuilder == null) {
mFormatBuilder = new StringBuilder(format.length() * 2);
}
}
/**
* Returns the current format string as set through {@link #setFormat}.
*/
@InspectableProperty
public String getFormat() {
return mFormat;
}
/**
* Sets the listener to be called when the chronometer changes.
*
* @param listener The listener.
*/
public void setOnChronometerTickListener(OnChronometerTickListener listener) {
mOnChronometerTickListener = listener;
}
/**
* @return The listener (may be null) that is listening for chronometer change
* events.
*/
public OnChronometerTickListener getOnChronometerTickListener() {
return mOnChronometerTickListener;
}
/**
* Start counting up. This does not affect the base as set from {@link #setBase}, just
* the view display.
*
* Chronometer works by regularly scheduling messages to the handler, even when the
* Widget is not visible. To make sure resource leaks do not occur, the user should
* make sure that each start() call has a reciprocal call to {@link #stop}.
*/
public void start() {
mStarted = true;
updateRunning();
}
/**
* Stop counting up. This does not affect the base as set from {@link #setBase}, just
* the view display.
*
* This stops the messages to the handler, effectively releasing resources that would
* be held as the chronometer is running, via {@link #start}.
*/
public void stop() {
mStarted = false;
updateRunning();
}
/**
* The same as calling {@link #start} or {@link #stop}.
* @hide pending API council approval
*/
public void setStarted(boolean started) {
mStarted = started;
updateRunning();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mVisible = false;
updateRunning();
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
//continue when view is hidden
visibility = View.VISIBLE;
super.onWindowVisibilityChanged(visibility);
mVisible = visibility == VISIBLE;
updateRunning();
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
updateRunning();
}
private synchronized void updateText(long now) {
mNow = now;
Log.e(TAG, "now: "+mNow + " mBase: "+mBase + " cost: "+(mNow - mBase));
long seconds = mCountDown ? mBase - now : now - mBase;
seconds /= 1000;
boolean negative = false;
if (seconds < 0) {
seconds = -seconds;
negative = true;
}
costSeconds = seconds;
String text = DateUtils.formatElapsedTime(mRecycle, seconds);
if (negative) {
text = getResources().getString(R.string.negative_duration, text);
}
if (mFormat != null) {
Locale loc = Locale.getDefault();
if (mFormatter == null || !loc.equals(mFormatterLocale)) {
mFormatterLocale = loc;
mFormatter = new Formatter(mFormatBuilder, loc);
}
mFormatBuilder.setLength(0);
mFormatterArgs[0] = text;
try {
mFormatter.format(mFormat, mFormatterArgs);
text = mFormatBuilder.toString();
} catch (IllegalFormatException ex) {
if (!mLogged) {
Log.w(TAG, "Illegal format string: " + mFormat);
mLogged = true;
}
}
}
setText(text);
}
private void updateRunning() {
boolean running = mVisible && mStarted && isShown();
if (running != mRunning) {
if (running) {
updateText(SystemClock.elapsedRealtime());
dispatchChronometerTick();
postDelayed(mTickRunnable, 1000);
} else {
removeCallbacks(mTickRunnable);
}
mRunning = running;
}
}
private final Runnable mTickRunnable = new Runnable() {
@Override
public void run() {
if (mRunning) {
updateText(SystemClock.elapsedRealtime());
dispatchChronometerTick();
postDelayed(mTickRunnable, 1000);
}
}
};
void dispatchChronometerTick() {
if (mOnChronometerTickListener != null) {
mOnChronometerTickListener.onChronometerTick(this);
}
}
private static final int MIN_IN_SEC = 60;
private static final int HOUR_IN_SEC = MIN_IN_SEC*60;
private static String formatDuration(long ms) {
int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS);
if (duration < 0) {
duration = -duration;
}
int h = 0;
int m = 0;
if (duration >= HOUR_IN_SEC) {
h = duration / HOUR_IN_SEC;
duration -= h * HOUR_IN_SEC;
}
if (duration >= MIN_IN_SEC) {
m = duration / MIN_IN_SEC;
duration -= m * MIN_IN_SEC;
}
final int s = duration;
final ArrayList<Measure> measures = new ArrayList<Measure>();
if (h > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
measures.add(new Measure(h, MeasureUnit.HOUR));
}
}
if (m > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
measures.add(new Measure(m, MeasureUnit.MINUTE));
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
measures.add(new Measure(s, MeasureUnit.SECOND));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
.formatMeasures(measures.toArray(new Measure[measures.size()]));
}
return "";
}
@SuppressLint("GetContentDescriptionOverride")
@Override
public CharSequence getContentDescription() {
return formatDuration(mNow - mBase);
}
@Override
public CharSequence getAccessibilityClassName() {
return Chronometer.class.getName();
}
public long getCostSeconds() {
return costSeconds;
}
}
这里会用到BaseActivityLifecycleCallbacks 这个类是管理callactivity的返回键的 ,返回上个页面,通话页面不会finish掉
public class MyAppLication extends Application {
public BaseActivityLifecycleCallbacks mLifecycleCallbacks;
public static MyAppLication application;
public static Context context;
private String TAG = "MyAppLication";
@Override
public void onCreate() {
super.onCreate();
application=this;
context= this;
mLifecycleCallbacks = new BaseActivityLifecycleCallbacks();
registerActivityLifecycleCallbacks();
}
private void registerActivityLifecycleCallbacks() {
this.registerActivityLifecycleCallbacks(mLifecycleCallbacks);
}
public BaseActivityLifecycleCallbacks getLifecycleCallbacks() {
return mLifecycleCallbacks;
}
}
public class BaseActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks, ActivityState {
public static final int STATUS_FORCE_KILLED = -1; //应用放在后台被强杀了
public static final int STATUS_NORMAL = 1; //APP正常态
//默认被初始化状态,被系统回收(强杀)状态
public int mAppStatus = STATUS_FORCE_KILLED;
public List<Activity> activityList = new ArrayList<>();
public List<Activity> resumeActivity = new ArrayList<>();
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
Log.e("ActivityLifecycle", "onActivityCreated " + activity.getLocalClassName());
activityList.add(0, activity);
}
@Override
public void onActivityStarted(Activity activity) {
Log.e("ActivityLifecycle", "onActivityStarted " + activity.getLocalClassName());
}
@Override
public void onActivityResumed(Activity activity) {
Log.e("ActivityLifecycle", "onActivityResumed " + activity.getLocalClassName());
if (!resumeActivity.contains(activity)) {
resumeActivity.add(activity);
if (resumeActivity.size() == 1) {
//do nothing
}
restartSingleInstanceActivity(activity);
}
}
@Override
public void onActivityPaused(Activity activity) {
Log.e("ActivityLifecycle", "onActivityPaused " + activity.getLocalClassName());
}
@Override
public void onActivityStopped(Activity activity) {
Log.e("ActivityLifecycle", "onActivityStopped " + activity.getLocalClassName());
resumeActivity.remove(activity);
if (resumeActivity.isEmpty()) {
Log.e("ActivityLifecycle", "在后台了");
}
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
Log.e("ActivityLifecycle", "onActivitySaveInstanceState " + activity.getLocalClassName());
}
@Override
public void onActivityDestroyed(Activity activity) {
Log.e("ActivityLifecycle", "onActivityDestroyed " + activity.getLocalClassName());
activityList.remove(activity);
}
@Override
public Activity current() {
return activityList.size() > 0 ? activityList.get(0) : null;
}
@Override
public List<Activity> getActivityList() {
return activityList;
}
@Override
public int count() {
return activityList.size();
}
@Override
public boolean isFront() {
return resumeActivity.size() > 0;
}
/**
* 跳转到目标activity
*
* @param cls
*/
public void skipToTarget(Class<?> cls) {
if (activityList != null && activityList.size() > 0) {
current().startActivity(new Intent(current(), cls));
for (Activity activity : activityList) {
activity.finish();
}
}
}
/**
* finish target activity
*
* @param cls
*/
public void finishTarget(Class<?> cls) {
if (activityList != null && !activityList.isEmpty()) {
for (Activity activity : activityList) {
if (activity.getClass() == cls) {
activity.finish();
}
}
}
}
/**
* 判断app是否在前台
*
* @return
*/
public boolean isOnForeground() {
return resumeActivity != null && !resumeActivity.isEmpty();
}
/**
* 用于按下home键,点击图标,检查启动模式是singleInstance,且在activity列表中首位的Activity
* 下面的方法,专用于解决启动模式是singleInstance, 为开启悬浮框的情况
*
* @param activity
*/
private void restartSingleInstanceActivity(Activity activity) {
boolean isClickByFloat = activity.getIntent().getBooleanExtra("isClickByFloat", false);
if(isClickByFloat) {
return;
}
//刚启动,或者从桌面返回app
if(resumeActivity.size() == 1 ) {
return;
}
//至少需要activityList中至少两个activity
if(resumeActivity.size() >= 1 && activityList.size() > 1) {
Activity a = getOtherTaskSingleInstanceActivity(resumeActivity.get(0).getTaskId());
if(a != null && !a.isFinishing() //没有正在finish
&& a != activity //当前activity和列表中首个activity不相同
&& a.getTaskId() != activity.getTaskId()
){
Log.e("ActivityLifecycle", "启动了activity = "+a.getClass().getName());
activity.startActivity(new Intent(activity, a.getClass()));
}
}
}
private Activity getOtherTaskSingleInstanceActivity(int taskId) {
if(taskId != 0 && activityList.size() > 1) {
for (Activity activity : activityList) {
if(activity.getTaskId() != taskId) {
if(isTargetSingleInstance(activity)) {
return activity;
}
}
}
}
return null;
}
private boolean isTargetSingleInstance(Activity activity) {
if(activity == null) {
return false;
}
CharSequence title = activity.getTitle();
/* if(TextUtils.equals(title, activity.getString(R.string.demo_activity_label_video_call))
|| TextUtils.equals(title, activity.getString(R.string.demo_activity_label_multi_call))) {
return true;
}*/
return false;
}
/**
* 此方法用于设置启动模式为singleInstance的activity调用
* 用于解决点击悬浮框后,然后finish当前的activity,app回到桌面的问题
* 需要如下两个权限:
* <uses-permission android:name="android.permission.GET_TASKS" />
* <uses-permission android:name="android.permission.REORDER_TASKS"/>
*
* @param activity
*/
public void makeMainTaskToFront(Activity activity) {
//当前activity正在finish,且可见的activity列表中只有这个正在finish的activity,且没有销毁的activity个数大于等于2
if (activity.isFinishing() && resumeActivity.size() == 1 && resumeActivity.get(0) == activity && activityList.size() > 1) {
ActivityManager manager = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> runningTasks = manager.getRunningTasks(20);
for (int i = 0; i < runningTasks.size(); i++) {
ActivityManager.RunningTaskInfo taskInfo = runningTasks.get(i);
ComponentName topActivity = taskInfo.topActivity;
//判断是否是相同的包名
if (topActivity != null && topActivity.getPackageName().equals(activity.getPackageName())) {
int taskId;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
taskId = taskInfo.taskId;
} else {
taskId = taskInfo.id;
}
//将任务栈置于前台
Log.e("ActivityLifecycle", "执行moveTaskToFront,current activity:" + activity.getClass().getName());
manager.moveTaskToFront(taskId, ActivityManager.MOVE_TASK_WITH_HOME);
}
}
}
}
}
public interface ActivityState {
/**
* 得到当前Activity
*
* @return
*/
Activity current();
/**
* 得到Activity集合
*
* @return
*/
List<Activity> getActivityList();
/**
* 任务栈中Activity的总数
*
* @return
*/
int count();
/**
* 判断应用是否处于前台,即是否可见
*
* @return
*/
boolean isFront();
}
最后就是我们的CallActivity
class CallActivity : AppCompatActivity() {
private lateinit var chronometer: MyChronometer // 通话计时
lateinit var binding: LayoutFlowwindowBinding
//来电或外呼
var isIncomingCall = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutFlowwindowBinding.inflate(layoutInflater)
setContentView(binding.root)
chronometer = findViewById(R.id.chronometer) as MyChronometer
// EaseCallFloatWindow(this)
CallFloatWindow.getInstance(this)
startCount()
binding.open.setOnClickListener {
showFloatWindow()
}
binding.close.setOnClickListener {
stopCount()
CallFloatWindow.getInstance(this).dismiss()
}
}
override fun onStart() {
super.onStart()
checkFloatIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
checkFloatIntent(intent)
}
override fun onStop() {
super.onStop()
showFloatWindow()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// 是否触发按键为back键
return if (keyCode == KeyEvent.KEYCODE_BACK) {
onBackPressed()
true
} else {
// 如果不是back键正常响应
super.onKeyDown(keyCode, event)
}
}
override fun onBackPressed() {
// 也可以处理成悬浮窗
AlertDialogUtils.show(this, "提示", "返回还是结束?", "返回",
{ dialog, which ->
showFloatWindow()
MyAppLication.application.getLifecycleCallbacks().makeMainTaskToFront(this)
},"结束"
) { dialog, which -> finish()}
}
fun isFloatWindowShowing(): Boolean {
return CallFloatWindow.getInstance().isShowing()
}
private fun startCount() {
if (chronometer != null) {
chronometer!!.base = SystemClock.elapsedRealtime()
chronometer!!.start()
}
}
private fun stopCount() {
if (chronometer != null) {
chronometer!!.stop()
}
}
private fun checkFloatIntent(intent: Intent) {
// 防止activity在后台被start至前台导致window还存在
if (isFloatWindowShowing()) {
val totalCostSeconds = CallFloatWindow.getInstance().totalCostSeconds
chronometer.base = SystemClock.elapsedRealtime() - totalCostSeconds * 1000
chronometer.start()
}
CallFloatWindow.getInstance().dismiss()
}
protected var requestOverlayPermission = false
protected val REQUEST_CODE_OVERLAY_PERMISSION = 1002
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_OVERLAY_PERMISSION && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Result of window permission request, resultCode = RESULT_CANCELED
if (Settings.canDrawOverlays(this)) {
doShowFloatWindow()
} else {
Toast.makeText(
this,
"悬浮窗权限 未授权",
Toast.LENGTH_SHORT
).show()
}
return
}
}
fun showFloatWindow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(this)) {
doShowFloatWindow()
} else { // To reqire the window permission.
if (!requestOverlayPermission) {
try {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
// Add this to open the management GUI specific to this app.
intent.data = Uri.parse("package:$packageName")
startActivityForResult(intent, REQUEST_CODE_OVERLAY_PERMISSION)
requestOverlayPermission = true
// Handle the permission require result in #onActivityResult();
} catch (e: Exception) {
e.printStackTrace()
}
}
}
} else {
doShowFloatWindow()
}
}
/**
* 显示悬浮窗
*/
fun doShowFloatWindow() {
// if (EaseCallKit.getInstance().getCallState() !== EaseCallState.CALL_ANSWERED) {
// ToastUtils.showLong("未接通时不能设置悬浮窗")
// return
// }
if (chronometer != null) {
CallFloatWindow.getInstance().setCostSeconds(chronometer.getCostSeconds())
}
CallFloatWindow.getInstance(MyAppLication.context)
.setmBase(chronometer.getBase())
CallFloatWindow.getInstance().show()
var surface = true
if (isIncomingCall) {
surface = false
}
CallFloatWindow.getInstance().update(!true, 0, 0, surface)
CallFloatWindow.getInstance().setCameraDirection(true, true)
moveTaskToBack(false)
}
override fun onDestroy() {
super.onDestroy()
CallFloatWindow.getInstance().dismiss()
}
}
总结:moveTaskToBack(false)方法是将activity放在后台,checkFloatIntent作用检测之前是否有悬浮窗,保持状态的恢复;
MyAppLication.application.getLifecycleCallbacks().makeMainTaskToFront(this)
这个方法是将当前页面挂起回到上个页面,一定要注意恢复。
翻译
搜索文章来源:https://www.toymoban.com/news/detail-789036.html
复制文章来源地址https://www.toymoban.com/news/detail-789036.html
到了这里,关于android 悬浮窗 模拟微信通话返回桌面悬浮的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!