Android实现系统级屏幕录制(上)
Android实现系统级屏幕录制
- 屏幕录制功能
- 整体流程图
- 控制面板内屏幕录制按钮的添加与实现
- 悬浮窗的实现
屏幕录制功能
上上周接到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/20 * @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 mCallbacks = new CopyOnWriteArrayList(); 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"?>
屏幕录制窗口的实现已经完毕了,下一篇会写下关乎开启录制和服务启动相关问题。
Android实现系统级屏幕录制(下)
更多相关文章
- Android护眼模式、屏幕亮度调节功能开发【基于Android8.0】
- Android专题之AsyncTask(三)异步方法操作之进度条
- Android Crash异常处理方法
- Android在网络中与JavaWeb的项目进行交互的方法(Webservice)
- android app耗电分析方法
- Android微信内按钮CSS失效
- Android真机测试,连接到本地服务器的方法
- 电池驱动调试总结,电池服务+电量计驱动+调试方法
- Android应用程序通用自动脱壳方法研究