android 悬浮窗 模拟微信通话返回桌面悬浮

这篇具有很好参考价值的文章主要介绍了android 悬浮窗 模拟微信通话返回桌面悬浮。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

现有一款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

到了这里,关于android 悬浮窗 模拟微信通话返回桌面悬浮的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 在 Android 上恢复已删除的通话记录 - 安卓手机通话记录恢复技巧

    有时,Android 用户会在内存空间用完时删除他们的通话记录。他们认为那些电话通话记录将不再需要了,但突然出于某些原因他们需要恢复那些已删除的通话记录。 恢复已删除的照片、视频、音乐、短信和通话记录等数据在以前是一件很难的事情。但是现在如果你想恢复一个

    2024年02月08日
    浏览(50)
  • Android 项目必备(四十四)-->Android 实现悬浮窗

    悬浮窗是一种比较常见的需求。例如把视频通话界面缩小成一个悬浮窗,然后用户可以在其他界面上处理事情。 本文将讲解悬浮窗实现步骤、原理、实例代码等 1. WindowMananger 接口 Android 的界面绘制,都是通过 WindowMananger 的服务来实现的。那么,既然要实现一个能够在自身应

    2024年02月12日
    浏览(38)
  • Android之 集成音视频通话

    一,背景 1.1 最近接收一个即时通讯二开项目,即时通讯部分用的XMPP协议,音视频则是集成的国外的开源免费库jitsi-meet-sdk-2.4.0-4.aar,是基于WebRTC的开源框架。但客户想要微信那种页面的排版,后来经研究jitsi是不能修改UI的,UI部分是用混合框架ReactNative写的,这样难度就大了

    2024年02月12日
    浏览(65)
  • Android NestedScrollView悬浮固定顶部

    项目中有页面涉及到多个元素组 需要NestedScrollView包裹来上下滑动 接到需求 一些标题在滑动到顶部时需要置顶 我之前做过关于Android Behavior之ViewPager+Fragment+RecyclerView实现吸顶效果 大概就是这种效果 只不过这次是随意的组件 比如 TextView 或布局组件 RelativeLayout 等 废话不多说来

    2024年01月24日
    浏览(43)
  • 怎么实现android 全局悬浮窗

    要实现 Android 全局悬浮窗,可以按照以下步骤: 在 AndroidManifest.xml 文件中添加 SYSTEM_ALERT_WINDOW 权限: 创建一个 Service 类,并在其中创建一个 WindowManager 来管理悬浮窗: 在悬浮窗 View 的布局文件中添加需要展示的内容: 在 Activity 中启动 Service: 最后,记得在不需要展示悬浮

    2024年02月11日
    浏览(44)
  • Android 应用弹出悬浮窗

    Android开发者经常遇到应用想弹出悬浮窗的操作,而且有可能还想要高层级弹窗,就像ipone的浮标touch一样。android当然也有类似的悬浮图标,比如前些年我们的流量监控提醒。  这里我们忽略UI美学,简单记录一下: 1、基本使用 它的基本使用步骤是不会变的,只是有时候我们

    2024年02月16日
    浏览(36)
  • WebRTC 系列(二、本地通话,H5、Android、iOS)

    有了上一篇 WebRTC 简介的基础,我们知道了 WebRTC 的工作流程,接下来就是需要用代码去实现这个流程了。对于不同端,实现起来的难易程度可能略微不同(实践中我感觉 iOS 端是资料最少的),但是整体流程是一样的。 问:要把大象装冰箱,总共分几步?答:三步。 初始化

    2024年02月11日
    浏览(44)
  • 【Android入门到项目实战-- 11.1】—— 实现悬浮按钮

            悬浮按钮是悬浮于界面之上,滑动屏幕时按钮不会跟随屏幕移动,它是Design Support库中提供的一个控件,这个控件可以帮助我们轻松实现悬浮按钮效果。         下面实现一个发布的悬浮按钮。        由于需要用到floatingActionButton,所以先导入,可参考这篇文

    2024年02月13日
    浏览(43)
  • Android 点击悬浮窗后台启动Activity问题及方案

    背景:开启悬浮窗,当app进入后台后,点击悬浮窗进入固定页面 问题:当app在后台运行时,点击悬浮窗,以下代码不能拉起app,经排查,部门手机需要开启后台弹出界面权限 如图  不同的机型这个权限的名称也不相同,要开启此权限,要跳转的界面也不同,所以需要获取不同机型的此页面路

    2024年02月11日
    浏览(44)
  • 微信小程序 实现可拖动悬浮图标

    效果图 (1)wxml (2)js (3)wxss

    2024年02月04日
    浏览(88)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包