camera预览悬浮窗实现

一,需求场景:

视频会议软件开启屏幕共享时有的会自动关闭本地摄像头视频预览,在这个时候我们想打开一个本地摄像头预览悬浮窗,并且会议软件重新打开本地视频,悬浮窗要消失。

实现以上需求分两步:

  • step 1:

    要准确监听屏幕共享的开关

  • step 2:

    本地视像头预览悬浮窗实现。包括拖动预览开启关闭等功能

二,监听屏幕共享的开关

安卓屏幕共享实现是通过MediaProjection捕获屏幕类容实现的,以下是一个简单的用例:

import android.content.Intent;
import android.media.MediaRecorder;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;
import android.os.Environment;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_CODE_CAPTURE = 1000;
    private MediaProjectionManager projectionManager;
    private MediaProjection mediaProjection;
    private MediaRecorder mediaRecorder;
    private boolean isRecording = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
        startScreenCapture();
    }

    private void startScreenCapture() {
        if (!isRecording) {
            startActivityForResult(projectionManager.createScreenCaptureIntent(), REQUEST_CODE_CAPTURE);
        } else {
            stopScreenCapture();
        }
    }

    private void stopScreenCapture() {
        if (mediaRecorder != null && isRecording) {
            mediaRecorder.stop();
            mediaRecorder.reset();
            mediaRecorder.release();
            mediaRecorder = null;
            isRecording = false;
            Toast.makeText(this, "Screen capture stopped", Toast.LENGTH_SHORT).show();
        }

        if (mediaProjection != null) {
            mediaProjection.stop();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == REQUEST_CODE_CAPTURE) {
            if (resultCode == RESULT_OK && data != null) {
                startRecording(data);
            } else {
                Toast.makeText(this, "Screen capture permission denied", Toast.LENGTH_SHORT).show();
                // Handle the case when the user denies screen capture permission
            }
        }
    }

    private void startRecording(Intent data) {
        mediaProjection = projectionManager.getMediaProjection(resultCode, data);
        mediaRecorder = new MediaRecorder();
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
        mediaRecorder.setOutputFile(Environment.getExternalStorageDirectory() + "/screen_record.mp4");

        int width = 1280; // Set desired width
        int height = 720; // Set desired height
        int dpi = 240; // Set desired DPI

        mediaRecorder.setVideoSize(width, height);
        mediaRecorder.setVideoFrameRate(30);
        mediaRecorder.setVideoEncodingBitRate(3000000);

        try {
            mediaRecorder.prepare();
        } catch (Exception e) {
            e.printStackTrace();
        }

        mediaProjection.createVirtualDisplay("MainActivity",
                width, height, dpi,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                mediaRecorder.getSurface(), null, null);

        mediaRecorder.start();
        isRecording = true;
        Toast.makeText(this, "Screen capture started", Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stopScreenCapture();
    }
}

开始屏幕捕获调用了mediaProjection.createVirtualDisplay(),停止屏幕捕获调用了mediaProjection.stop()。一开始我是在MediaProjection.java这两个方法中监听是否开启屏幕共享,但后来发现大部分会议app停止共享按钮是调用了stop()方法,但是结束会议却没有调用stop方法,这个场景监听这两个方法就是不可行的。
后面发现开启结束屏幕录制都会打印以下日志:
位置:frameworks/base/services/core/java/com/android/server/display/DisplayDeviceRepository.java
日志:Display device added:xxxxxxxxx ,Display device removed:xxxxxxx
以上日志意思是添加移除了一个display,后面了解到屏幕捕获添加移除的display类型是Display.TYPE_VIRTUAL。

Display.TYPE_VIRTUAL 是 Android 中的一个常量,它代表虚拟显示屏幕的类型。虚拟显示屏幕是在不需要物理显示屏幕的情况下创建的,允许应用程序进行图形渲染和图像处理等操作,而无需实际显示在物理屏幕上。
这个常量一般用于 VirtualDisplay 类的创建。VirtualDisplay 可以让你在 Android 设备上创建一个虚拟显示屏,并在这个虚拟显示屏上渲染内容,比如视频播放、屏幕录制、演示文稿等。虚拟显示屏幕不会在实际的物理屏幕上显示内容,而是供应用程序自行处理和操作。
TYPE_VIRTUAL 常量代表虚拟显示屏幕的类型,这种显示屏通常是通过 MediaProjection 和 MediaProjectionManager 创建的,用于屏幕捕获、录制和其它图像处理操作。

所以这个类型的display是符合屏幕共享场景的。最终监听实现如下:

private void handleDisplayDeviceAdded(DisplayDevice device) {
        synchronized (mSyncRoot) {
            DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
            if (mDisplayDevices.contains(device)) {
                Slog.w(TAG, "Attempted to add already added display device: " + info);
                return;
            }
            // add by cjx
            onScreenShareDisplayAddOrRemoved(true, info.type);
            // end
            Slog.i(TAG, "Display device added: " + info);
            device.mDebugLastLoggedDeviceInfo = info;

            mDisplayDevices.add(device);
            sendEventLocked(device, DISPLAY_DEVICE_EVENT_ADDED);
        }
    }

private void handleDisplayDeviceRemoved(DisplayDevice device) {
        synchronized (mSyncRoot) {
            DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
            if (!mDisplayDevices.remove(device)) {
                Slog.w(TAG, "Attempted to remove non-existent display device: " + info);
                return;
            }

            // add by cjx
            onScreenShareDisplayAddOrRemoved(false, info.type);
            // end
            Slog.i(TAG, "Display device removed: " + info);
            device.mDebugLastLoggedDeviceInfo = info;
            sendEventLocked(device, DISPLAY_DEVICE_EVENT_REMOVED);

        }
    }

// 通知应用显示悬浮窗

    String ACTION_START_CAMERA_FLOAT_WINDOW = "xxxxxxxxxxxxx.service.start_window_show";
    String ACTION_STOP_CAMERA_FLOAT_WINDOW = "xxxxxxxxxxxxxx.service.stop_window_show";
    String PKG_NAME_CAMERA_TOOL = "com.kandaovr.meeting.camera";


    private void onScreenShareDisplayAddOrRemoved(boolean add, int type){
        if(type != Display.TYPE_VIRTUAL){
            Slog.d(TAG,"is add" + add +"--type:" + type);
            return;
        }
        Log.i(TAG, "onScreenShareDisplayAddOrRemoved isAdd:" + add);
        mHandler.postDelayed(() -> {
            Intent intent = new Intent();
            intent.setPackage(PKG_NAME_CAMERA_TOOL);
            if (add) {
                intent.setAction(ACTION_START_CAMERA_FLOAT_WINDOW);
            } else {
                intent.setAction(ACTION_STOP_CAMERA_FLOAT_WINDOW);
            }
            mContext.startService(intent);
        },100);
    }

三,camera悬浮窗实现

在这里插入图片描述

在这里插入图片描述

1. 悬浮窗这里采用继承dialog并设置dialog window的类型为TYPE_APPLICATION_OVERLAY(可悬浮于所有应用之上)

      private void initWindow(Context context) {
        Window window = getWindow();
        window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
        window.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
        // 设置不响应焦点,点击外部不消失
        window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);

        wmParams = window.getAttributes();
        wmParams.format = PixelFormat.TRANSLUCENT; // 设置背景透明
        wmParams.gravity = Gravity.START | Gravity.TOP; //影响set x y
        wmParams.alpha = 1.0f;
        wmParams.dimAmount = 0.0f; //dimAmount在0.0f和1.0f之间,0.0f完全不暗,1.0f全暗

        DisplayMetrics displayMetrics = new DisplayMetrics();
        window.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        int screenWidth = displayMetrics.widthPixels;
        int screenHeight = displayMetrics.heightPixels;
        Log.i(TAG, String.format("initWindow w:%d, h:%d ", screenWidth, screenHeight));

        int viewRootWidth = getContext().getResources().getDimensionPixelSize(R.dimen.dp_315);
        int viewRootHeight = getContext().getResources().getDimensionPixelSize(R.dimen.dp_216);
        int margin = getContext().getResources().getDimensionPixelSize(R.dimen.dp_30);

        Log.i(TAG, String.format("initWindow viewRootWidth:%d, viewRootHeight:%d ", viewRootWidth, viewRootHeight));
        // 设置dialog出现的初始位置
        wmParams.x = screenWidth - viewRootWidth - margin;
        wmParams.y = screenHeight - viewRootHeight - margin;
        window.setAttributes(wmParams);
        mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);

    }

2. 悬浮窗的拖拽事件,监听Touch事件拖动的位置不断设置window位置

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_MOVE:
                int mMoveWindowX = (int) event.getRawX();
                int mMoveWindowY = (int) event.getRawY();
                mCurrentPoint.x = mMoveWindowX;
                mCurrentPoint.y = mMoveWindowY;
                doActionMove(mMoveWindowX, mMoveWindowY);
                return true;
            case MotionEvent.ACTION_DOWN:
                mDownPoint.x = (int) event.getRawX();
                mDownPoint.y = (int) event.getRawY();
                mCurrentPoint.x = mDownPoint.x;
                mCurrentPoint.y = mDownPoint.y;
                mDownMills = System.currentTimeMillis();
                Log.i(TAG, String.format("ACTION_DOWN: (%d,%d)", mCurrentPoint.x, mCurrentPoint.y));
                break;
            case MotionEvent.ACTION_UP:
                int upX = (int) event.getRawX();
                int upY = (int) event.getRawY();
                doUpAction(upX, upY);
                return true;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 当参数值包含Gravity.LEFT时,对话框出现在左边,所以params.x就表示相对左边的偏移
     * 当参数值包含Gravity.RIGHT时,对话框出现在右边,所以params.x就表示相对右边的偏移
     * 当参数值包含Gravity.TOP时,对话框出现在上边,所以params.y就表示相对上边的偏移
     * 当参数值包含Gravity.BOTTOM时,对话框出现在下边,所以params.y就表示相对下边的偏移
     */
    private synchronized void doActionMove(int mMoveWindowX, int mMoveWindowY) {
        if (Math.abs(mDownPoint.x - mCurrentPoint.x) < 5 && Math.abs(mDownPoint.y - mCurrentPoint.y) < 5) {
            return;
        }
        /*更新拖拽view位置 view中心位置移动到触摸点*/
        Log.i(TAG, String.format("doActionMove mMoveWindowX:%d, mMoveWindowY:%d", mMoveWindowX, mMoveWindowY));
        int x = (int) (mMoveWindowX - mDragViewWidth / 2.0f);
        int y = (int) (mMoveWindowY - mDragViewHeight / 2.0f);
        wmParams.x = x;
        wmParams.y = y;
        mWindowManager.updateViewLayout(getWindow().getDecorView().getRootView(), wmParams);
    }

3. 悬浮窗只有一个按钮的拖动和点击
在这里插入图片描述
将按钮的触摸事件交由dialog处理

mBinding.imgFold.setOnTouchListener(this);

@Override
    public boolean onTouch(View v, MotionEvent event) {
        if (!foldOpen) {
            onTouchEvent(event);
            return true;
        }
        return false;
    }

同时在dialog的触摸事件甄别出点击的UP事件触发performClick();

   private void doUpAction(int upX, int upY) {
        if (Math.abs(mDownPoint.x - mCurrentPoint.x) < 5 && Math.abs(mDownPoint.y - mCurrentPoint.y) < 5) {
            Logger.d(TAG, "doUpAction foldOpen: " + foldOpen);
            if (!foldOpen) {
                mBinding.imgFold.performClick();
            }
        }
    }

4. 预览TextureView圆角实现

 int radius = getContext().getResources().getDimensionPixelSize(R.dimen.dp_10);
 textureView.setOutlineProvider(new TextureViewOutlineProvider(radius));
 textureView.setClipToOutline(true);

 public static class TextureViewOutlineProvider extends ViewOutlineProvider {
        private int radius;

        public TextureViewOutlineProvider(int radius) {
            this.radius = radius;
        }

        @Override
        public void getOutline(View view, Outline outline) {
            Rect rect = new Rect();
            view.getGlobalVisibleRect(rect);
            Rect selfRect = new Rect(0, 0,
                    rect.right - rect.left,
                    rect.bottom - rect.top);
            outline.setRoundRect(selfRect, radius);
        }
    }

5. 悬浮窗大小变换动画
这里直接对跟布局进行layout变换,对view进行显示隐藏的控制,通过给根布局设置
LayoutTransition过度动画实现平滑过度效果

   private void reLayoutByCloseState() {
        FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mBinding.llRootView.getLayoutParams();
        mCameraOpen = false;
//        mBinding.imgPreviewEnable.setVisibility(foldOpen ? View.VISIBLE : View.GONE);
        Log.i(TAG, "reLayoutByCloseState foldOpen: " + foldOpen);

        if (foldOpen) {
            // 打开--camera关闭状态
            params.width = (int) getContext().getResources().getDimension(R.dimen.dp_315);
            params.height = (int) getContext().getResources().getDimension(R.dimen.dp_36);
            mBinding.imgPreviewEnable.setVisibility(View.VISIBLE);
            mBinding.rlSurfaceRoot.setVisibility(View.VISIBLE);

            mBinding.imgFold.setImageResource(R.drawable.icon_close_x);

        } else {
            // 关闭
            params.width = (int) getContext().getResources().getDimension(R.dimen.dp_50);
            params.height = (int) getContext().getResources().getDimension(R.dimen.dp_50);
            mBinding.imgPreviewEnable.setVisibility(View.GONE);
            mBinding.rlSurfaceRoot.setVisibility(View.GONE);
            mBinding.imgFold.setImageResource(R.drawable.icon_window_folded);
        }

        // 设置布局动画
        LayoutTransition transition = new LayoutTransition();
        mBinding.llRootView.setLayoutTransition(transition);
        mBinding.llRootView.setLayoutParams(params);
        initRootViewSize();
        closeCamera();
    }


    private void reLayoutByCameraState() {
        mBinding.rlSurfaceRoot.setVisibility(mCameraOpen ? View.VISIBLE : View.GONE);
        FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mBinding.llRootView.getLayoutParams();
        Log.i(TAG, "reLayoutByCameraStatem CameraOpen: " + mCameraOpen);
        mBinding.imgPreviewEnable.setImageResource(mCameraOpen ? R.drawable.video_open : R.drawable.video_close);
        if (mCameraOpen) {
            params.width = (int) getContext().getResources().getDimension(R.dimen.dp_315);
            params.height = (int) getContext().getResources().getDimension(R.dimen.dp_216);
        } else {
            params.width = (int) getContext().getResources().getDimension(R.dimen.dp_315);
            params.height = (int) getContext().getResources().getDimension(R.dimen.dp_36);
        }
        LayoutTransition transition = new LayoutTransition();
        mBinding.llRootView.setLayoutTransition(transition);
        mBinding.llRootView.setLayoutParams(params);
        initRootViewSize();
    }

xml布局需要给父布局设置 android:animateLayoutChanges="true"属性

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <androidx.appcompat.widget.LinearLayoutCompat
        android:id="@+id/ll_rootView"
        android:layout_width="@dimen/dp_315"
        android:layout_height="@dimen/dp_216"
        android:animateLayoutChanges="true"
        android:background="@drawable/shape_float_window_bg"
        android:gravity="center"
        android:orientation="vertical">

        <RelativeLayout
            android:id="@+id/ll_bts"
            android:layout_width="match_parent"
            android:layout_height="@dimen/dp_36"
            android:animateLayoutChanges="true"
            android:gravity="center"
            app:layout_constraintTop_toTopOf="parent">

            <ImageView
                android:padding="@dimen/dp_8"
                android:id="@+id/img_preview_enable"
                android:layout_width="@dimen/dp_36"
                android:layout_height="@dimen/dp_36"
                android:src="@drawable/video_open" />

            <ImageView
                android:id="@+id/img_fold"
                android:padding="@dimen/dp_8"
                android:layout_width="@dimen/dp_36"
                android:layout_height="@dimen/dp_36"
                android:layout_alignParentEnd="true"
                android:src="@drawable/icon_close_x" />

        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/rl_surfaceRoot"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@drawable/shape_float_surface_bg">

            <TextureView
                android:id="@+id/surfaceView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_margin="1dp"
                android:layerType="hardware" />
        </RelativeLayout>

    </androidx.appcompat.widget.LinearLayoutCompat>
</FrameLayout>

完结!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值