Android实现系统级屏幕录制(上)

2,250 阅读7分钟

屏幕录制功能

上上周接到ROM的定制需求,需要完成一个屏幕录制功能,所以就跟两个同事商讨并完成该功能开发。写这篇文章主要想分享给大家我自己的实现方法和记录自己所做过的东西吧。注:屏幕录制在Android9.0之前只提供了对应的API,并没有功能实现。该功能基于Android9.0完成,而Android10.0已经有了原生的屏幕录制,有兴趣的也可以看看源码了解下。

整体流程图

大致流程图如此,通过判断按钮的点击,设置一状态值的改变,通过过判断值的改变,从而使悬浮窗实现类通过单例实现窗体的显示和隐藏。在悬浮窗中通过开启录屏方法去申请权限并启动service服务启动录屏。接下来我会详细讲解下如何实现。

控制面板内屏幕录制按钮的添加与实现

首先我们得清楚快捷按钮的实现(qs)。关于控制面板的加载流程可以后续再出一篇文章详细讲解下。

1.配置项添加字段

framework\base\packages\SystemUI\res\values\config.xml

<string name="quick_settings_tiles_default" translatable="false">
 wifi,bt,dnd,flashlight,rotation,battery,cell,airplane,cast
    ,screenrecord</string>
<string name="quick_settings_tiles_stock" translatable="false">
 wifi,bt,dnd,flashlight,rotation,battery,cell,airplane,cast
    ,screenrecord</string>

在此添加的目的在于快捷面板的预置按钮添加会根据配置项的字段进行匹配并添加控件于面板内。

2.屏幕录制按钮初始化

framework\base\packages\SystemUI\src\com\android\systemui\qs\tileimpl\QSFactoryImpl.java

在createTileInternal()方法中通过字段的筛选实例化对应的Tile类。

private QSTileImpl createTileInternal(String tileSpec) {
	case "screenrecord":
          return new ScreenRecordTile(mHost);
    }

ScreenRecordTile类存放路径:

framework\base\packages\SystemUI\src\com\android\systemui\qs\tiles\ScreenRecordTile.java

该ScreenRecordTile类需要继承QSTileImpl类并重写其中的方法。主要有:getLongClickIntent()长点击跳转、handleClick()点击事件处理、getTileLabel()快捷方法的按钮名称、handleUpdateState()状态更新处理。在点击事件中我们通过对某一字段值进行判断并赋值,以便其他地方获取该值后进行相对应操作。

package com.android.systemui.qs.tiles;

import android.content.ContentResolver;
import android.content.Intent;
import android.content.Context;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.quicksettings.Tile;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.R;

public class ScreenRecorderTile extends QSTileImpl<QSTile.BooleanState>{
 private ContentResolver mContentResolver=null;
 private static final String SCREEN_RECORDER_SETTINGS_ACTION="android.settings.SCREEN_RECORDER";
    public ScreenRecorderTile(QSHost host){
        super(host);
        mContentResolver = mContext.getContentResolver();
    }

    @Override
    protected void handleClick() {
     if(0 == Settings.System.getInt(mContentResolver,Settings.System.SCREEN_RECORDERD_ENABLED,0)){
            Settings.System.putIntForUser(mContentResolver,Settings.System.SCREEN_RECORDERD_ENABLED,1, UserHandle.USER_CURRENT);
        }else if(1 == Settings.System.getInt(mContentResolver,Settings.System.SCREEN_RECORDERD_ENABLED,0)){
            Settings.System.putIntForUser(mContentResolver,Settings.System.SCREEN_RECORDERD_ENABLED,0, UserHandle.USER_CURRENT);
        refreshState();
    }

    @Override
    protected void handleUpdateState(BooleanState state, Object arg) {
        state.icon=ResourceIcon.get(R.drawable.screen_record_icon);
    }

    @Override
    public Intent getLongClickIntent() {
        return new Intent();
    }
    
    @Override
    public CharSequence getTileLabel() {
        return mContext.getString(R.string.quick_settings_screen_record);
    }
}

悬浮窗的实现

我们需要添加控制类和控制实现类完成回调接口,并实现我们需要的任务。 首先我们先写个接口和方法。

framework\base\packages\SystemUI\src\com\android\systemui\statusbar\policy\ScreenRecordStatusController.java

package com.android.systemui.statusbar.policy;

/**
 * @module  ScreenRecord
 * @author  San-Nan
 * @date  2019/12/10
 * @description  屏幕录制控制器
 */

public interface ScreenRecordStatusController extends Listenable,
        CallbackController<ScreenRecordStatusController.ScreenRecordStatusControllerCallback>{

    boolean isRecordedShow();

    interface ScreenRecordStatusControllerCallback {
        void onScreenRecordedChanged(boolean enableShow);//屏幕录制开关
    }
}

编写需要实现的帮助类,主要完成监听注册和取消。

framework\base\packages\SystemUI\src\com\android\systemui\statusbar\policy\ScreenRecordStatusPolicy.java

package com.android.systemui.statusbar.policy;

import android.content.Context;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.UserHandle;
import android.provider.Settings;
import com.android.systemui.util.ScreenRecordUtils;
import android.net.Uri;

/**
 * @module  ScreenRecord
 * @author  San-Nan
 * @date  2019/12/10
 * @description  Provides helper functions for configuring the display ScreenRecord policy.
 */
public class ScreenRecordStatusPolicy {

    private static final String TAG = "ScreenRecordStatusPolicy";

    public ScreenRecordStatusPolicy(){}

    public static boolean isScreenRecordedEnable(Context context){
        return ScreenRecordUtils.isScreenRecordedEnable(context);
    }

    public static void registerScreenPolicyListener(Context context,
                                                         ScreenRecordStatusPolicyListener listener) {
        registerScreenPolicyListener(context, listener, UserHandle.getCallingUserId());
    }

    public static void registerScreenPolicyListener(Context context,
                                                         ScreenRecordStatusPolicyListener listener, int userHandle) {
        context.getContentResolver().registerContentObserver(
                Settings.System.getUriFor(Settings.System.SCREEN_RECORDERD_ENABLED),false,
                listener.mScreenRecordedObserver,context.getUserId());
    }

    public static void unregisterScreenPolicyListener(Context context,
                                                        ScreenRecordStatusPolicyListener listener) {
        context.getContentResolver().unregisterContentObserver(listener.mScreenRecordedObserver);

    }

    public static abstract class ScreenRecordStatusPolicyListener {
        final ContentObserver mScreenRecordedObserver = new ContentObserver(new Handler()) {
            @Override
            public void onChange(boolean selfChange,Uri uri){
                ScreenRecordStatusPolicyListener.this.onScreenRecordedChanged();
            }
        };
        public abstract void onScreenRecordedChanged();
    }
}

可以看到其中的用通过工具类ScreenRecordUtils 的isScreenRecordedEnable 方法判断。

framework\base\packages\SystemUI\src\com\android\systemui\util\ScreenRecordUtils.java

package com.android.systemui.util;

import android.content.Context;
import android.provider.Settings;

public class ScreenRecordUtils {
    public static boolean isScreecreennRecordedEnable(Context context){
        try {
            int screenRecordedEnable = Settings.System.getInt(context.getContentResolver(), Settings.System.SCREEN_RECORDERD_ENABLED, 0);
            if(screenRecordedEnable == 1){
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
}

isScreecreennRecordedEnable通过判断"SCREEN_RECORDERD_ENABLED"的值返回true or false。

接下来完成实现类。

package com.android.systemui.statusbar.policy;

import android.content.Context;
import android.os.UserHandle;

import java.util.concurrent.CopyOnWriteArrayList;
import android.net.Uri;

/**
 * @module  ScreenRecord
 * @author  San-Nan
 * @date  2019/12/10
 * @description  屏幕录制控制器实现类
 */
public class ScreenRecordStatusControllerImpl implements  ScreenRecordStatusController {

    private final Context mContext;
    private final CopyOnWriteArrayList<ScreenRecordStatusControllerCallback> mCallbacks =
            new CopyOnWriteArrayList<ScreenRecordStatusControllerCallback>();

    private final ScreenRecordStatusPolicy.ScreenRecordStatusPolicyListener mScreenRecordStatusPolicyListener =
            new ScreenRecordStatusPolicy.ScreenRecordStatusPolicyListener() {
                @Override
                public void onScreenRecordedChanged(){
                    notifyScreenRecordedChanged();
                }
            };

    public ScreenRecordStatusControllerImpl(Context context) {
        mContext = context;
        setListening(true);
    }

    @Override
    public void setListening(boolean listening) {
        if (listening) {
            ScreenRecordStatusPolicy.registerScreenPolicyListener(mContext, mScreenRecordStatusPolicyListener,
                    UserHandle.USER_CURRENT);
        } else {
            ScreenRecordStatusPolicy.unregisterScreenPolicyListener(mContext, mScreenRecordStatusPolicyListener);
        }
    }

    private void notifyScreenRecordedChanged(){
        for(ScreenRecordStatusControllerCallback callback : mCallbacks) {
            notifyScreenRecordedChanged(callback);
        }
    }
    private void notifyScreenRecordedChanged(ScreenRecordStatusControllerCallback callback) {
        callback.onScreenRecordedChanged(isRecordedShow());
    }

    @Override
    public boolean isRecordedShow() {
        return ScreenRecordStatusPolicy.isScreenRecordedEnable(mContext);
    }

    @Override
    public void addCallback(ScreenRecordStatusController.ScreenRecordStatusControllerCallback listener) {
        if(mCallbacks.contains(listener)){
            return;
        }
        mCallbacks.add(listener);
        notifyScreenRecordedChanged(listener);//屏幕录制开关
    }

    @Override
    public void removeCallback(ScreenRecordStatusController.ScreenRecordStatusControllerCallback listener) {
        if(!mCallbacks.contains(listener)){
            return;
        }
        mCallbacks.remove(listener);
    }
}

接口、帮助、实现都完成了,下一步我们开始使用它们。 SystemUI中有个类为Dependency类,用于手动添加依赖项,并且存在于整个SystemUI的生命周期中。 在start()方法中添加刚才我们写的控制器和实现类。

framework\base\packages\SystemUI\src\com\android\systemui\Dependency.java

public void start() {
 mProviders.put(ScreenRecordStatusController.class,() ->
                new ScreenRecordStatusControllerImpl(mContext));
}

在StatusBar类中的start()和destroy()添加和移除回调。并在回调方法中写入我们需要操作的内容。

framework\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\StatusBar.java

public void start() {
 Dependency.get(ScreenRecordStatusController.class).addCallback(mScreenRecordStatusControllerCallback);
 }

public void destroy() {
 Dependency.get(ScreenRecordStatusController.class).removeCallback(mScreenRecordStatusControllerCallback);
 }

ScreenRecordStatusController.ScreenRecordStatusControllerCallback mScreenRecordStatusControllerCallback = new ScreenRecordStatusController.ScreenRecordStatusControllerCallback() {
    @Override
    public void onScreenRecordedChanged(boolean enableShow){
        if(enableShow){
            FloatScreenRecordedWindow.getInstance(mContext);
            //    FloatScreenRecordedWindow.getInstance(mContext).setStatusBar(StatusBar.this,mHandler);
            FloatScreenRecordedWindow.getInstance(mContext).floatScreenRecordedWinShow(true);
        }
        else{
            FloatScreenRecordedWindow.getInstance(mContext).floatScreenRecordedWinShow(false);
        }
    }
};

在onScreenRecordedChanged()方法中通过判断SCREEN_RECORDERD_ENABLED 数值来决定是否展示悬浮窗。

悬浮窗window

framework\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\FloatScreenRecordedWindow.java

package com.android.systemui.statusbar.phone;

import android.app.Service;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.graphics.PixelFormat;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.StatFs;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.app.Instrumentation;
import android.view.KeyEvent;

import com.android.systemui.R;

import android.view.ViewGroup;

import android.view.MotionEvent;

import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.CommandQueue.Callbacks;
import com.android.systemui.statusbar.stack.StackStateAnimator;

import android.app.ActivityManager;

import com.android.systemui.SysUiServiceProvider;
import com.android.internal.util.LatencyTracker;

import java.util.List;

import android.text.format.Formatter;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.content.Intent;
import java.util.Random;
import android.view.Display;
import android.graphics.Point;
import android.util.DisplayMetrics;
import android.text.TextUtils;

/**
 * @module  ScreenRecord
 * @author  San-Nan
 * @date  2019/12/10
 * @description  屏幕录制悬浮窗
 */
public class FloatScreenRecordedWindow {
    private HandlerThread mThread = null;
    private Context mContext = null;
    private ContentResolver mContentResolver;
    private FloatScreenRecordedWindowHandler floatScreenRecordedWindowHandler = null;

    private static final String TAG = "FloatScreenRecordedWindow";
    private static final boolean DEBUG = true;
    private static final int SHOW_VIEW_MSG = 0x00;//显示悬浮窗
    private static final int KILL_VIEW_MSG = 0x01;//隐藏悬浮窗

    private View floatRecordedView;
    private WindowManager mWindowManager;
    /**
     * 悬浮窗控件
     */
    private LinearLayout lScreenRecorded;

    private static FloatScreenRecordedWindow floatScreenRecordedWindow = null;

    private WindowManager.LayoutParams lp = new WindowManager.LayoutParams();

    private CommandQueue mCommandQueue;
    private boolean longPress = false;
    private GestureDetector mDetector;
    private StatusBar mStatusBar;
    private Handler mHandler;
    private ActivityManager activityManger;
    private Display display;
    private Point point;

    private FloatScreenRecordedWindow(Context context) {
        mContext = context;
        init();
    }

    public synchronized static FloatScreenRecordedWindow getInstance(Context context) {
        if (floatScreenRecordedWindow == null) {
            floatScreenRecordedWindow = new FloatScreenRecordedWindow(context);
        }
        return floatScreenRecordedWindow;
    }

    public void init() {
        mWindowManager = (WindowManager) mContext
                .getSystemService(Context.WINDOW_SERVICE);
        activityManger = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
        display = mWindowManager.getDefaultDisplay();
        mContentResolver = mContext.getContentResolver();
        point = new Point();
        mThread = new HandlerThread("FloatScreenRecordedThread");
        mThread.start();
        floatScreenRecordedWindowHandler = new FloatScreenRecordedWindowHandler(mThread.getLooper());
    }

    public void floatScreenRecordedWinShow(boolean show) {
        if (show) {
            floatScreenRecordedWindowHandler.sendEmptyMessage(SHOW_VIEW_MSG);
        } else {
            floatScreenRecordedWindowHandler.sendEmptyMessage(KILL_VIEW_MSG);
        }
    }

    private class FloatScreenRecordedWindowHandler extends Handler {
        public FloatScreenRecordedWindowHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case SHOW_VIEW_MSG://显示悬浮窗
                    showRecordedWin();
                    viewEvent();
                    break;
                case KILL_VIEW_MSG://隐藏悬浮窗
                    if (null != floatRecordedView) {
                        mWindowManager.removeView(floatRecordedView);
                        floatRecordedView = null;
                        floatScreenRecordedWindow = null;
                    }
                    break;
            }
        }
    }

    private void showRecordedWin() {
        /* create a view and attach it to Window Manager */
        LayoutInflater inflater = (LayoutInflater) mContext
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        floatRecordedView = inflater.inflate(R.layout.screen_recorded, null);
        mWindowManager.addView(floatRecordedView, getLayoutParams());
    }

    private WindowManager.LayoutParams getLayoutParams() {
        lp.type = WindowManager.LayoutParams.TYPE_PHONE;
        lp.format = PixelFormat.RGBA_8888;
        lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
        lp.gravity = Gravity.CENTER_VERTICAL;
        lp.width = 305;
        lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
        display.getRealSize(point);
        int displayWidth = point.x;
        int displayHeight = point.y;
        lp.x = displayWidth / 2;
        lp.y = 0;
        return lp;
    }

    private void viewEvent() {
        lScreenRecorded = (LinearLayout) floatRecordedView.findViewById(R.id.screen_recorded);
        lScreenRecorded.setOnTouchListener(lScreenRecordedWindowTouchListener);
        lScreenRecorded.setOnLongClickListener(lScreenRecordedWindowLongListener);
    }

    //悬浮窗事件
    View.OnTouchListener lScreenRecordedWindowTouchListener = new View.OnTouchListener() {
        float lastX = 0.0f;
        float lastY = 0.0f;
        float nowX = 0.0f;
        float nowY = 0.0f;
        float tranX = 0.0f;
        float tranY = 0.0f;
        boolean isLongPressAndMove = false;
        boolean goneBall = false;

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    lastX = event.getRawX();
                    lastY = event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    nowX = event.getRawX();
                    nowY = event.getRawY();
                    tranX = nowX - lastX;
                    tranY = nowY - lastY;
                    if (longPress) {//长按拖动
                        display.getRealSize(point);
                        int displayWidth = point.x;
                        int displayHeight = point.y;
                        int viewY = (int) floatRecordedView.getHeight() / 2;
                        displayHeight = displayHeight - viewY;
                        if (nowY > displayHeight) {
                            lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                                    | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
                                    | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
                            goneBall = true;
                        } else {
                            lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                                    | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
                                    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
                        }
                        lp.x += tranX;
                        lp.y += tranY;
                        mWindowManager.updateViewLayout(floatRecordedView, lp);
                        if (Math.max(Math.abs(tranX), Math.abs(tranX)) > 2.0f) {//长按释放不灵敏问题,移动距离小于5.0f,则执行长按释放
                            isLongPressAndMove = true;
                        }
                    }
                    lastX = nowX;
                    lastY = nowY;
                    break;
                case MotionEvent.ACTION_UP:
                    nowX = event.getRawX();
                    nowY = event.getRawY();
                    tranX = nowX - lastX;
                    tranY = nowY - lastY;
                    int viewY = (int) floatRecordedView.getHeight() / 2;
                    int displayHeight = point.y;
                    displayHeight = displayHeight - viewY;
                    if (nowY < displayHeight) {
                        goneBall = false;
                    }
                    longPress = false;
                    isLongPressAndMove = false;
                    break;
            }
            return false;
        }
    };

    View.OnLongClickListener lScreenRecordedWindowLongListener = new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View view) {
            // TODO Auto-generated method stub
            longPress = true;
            return false;
        }
    };
}

在floatScreenRecordedWinShow () 方法中由show值来决定发送显示或者隐藏的message,再交由handleMessage()处理。在showRecordedWin() 中加载悬浮窗布局文件screen_recorded。

scree_recorded.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <LinearLayout
        android:id="@+id/screen_recorded"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:background="@drawable/bg_recorded_circle"
        android:gravity="center"
        android:orientation="horizontal">

        <!--播放按钮-->
        <LinearLayout
            android:id="@+id/record_start_button"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:gravity="center"
            android:orientation="horizontal"
            android:visibility="visible">

            <View
                android:layout_width="2dp"
                android:layout_height="match_parent"
                android:layout_marginLeft="5dp"
                android:layout_marginRight="5dp"
                android:textColor="#000000" />

            <ImageView
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:background="@drawable/shape_reverse_triangle"
                android:rotation="270" />

        </LinearLayout>

        <View
            android:layout_width="1dp"
            android:layout_height="fill_parent"
            android:layout_marginTop="5dp"
            android:layout_marginRight="6dp"
            android:layout_marginBottom="5dp"
            android:background="#FFFFFFFF" />

        <TextView
            android:id="@+id/record_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="00:00:00"
            android:textColor="#ffffff"
            android:textSize="15dp" />

    </LinearLayout>
</LinearLayout>

屏幕录制窗口的实现已经完毕了,下一篇会写下关乎开启录制和服务启动相关问题。