Android 录屏原来可以这么优雅

都 2020 年了,听说 Android 还没有自带录屏功能,当项目经理把你一顿吊打的时候问你为啥 IOS 本身就自带这个功能,
你竟然无言以对,你说再等等,最近看到 android11 预览版已经发布,看介绍说明之前砍掉的录屏功能将在此版本正式回归,
就自带了
3OzeYt.jpg

经理说现在大多用的还是android8、9的版本,不能再等了,现在就开始盘它。
好嘛,既然要求搞,那就要弄得优雅些,想到优雅,那必须是这样。

3XnMcQ.jpg

来康康顺着这种优雅的思路最终实现的效果,上图走起

3OzKl8.png

恩哼哼,竟然有人说静图不够优雅,满足你,上个 gif 镇楼

3OzQOg.gif

那么要实现这么优雅的效果,你需要怎么做呢?来来来,先康康实现流程图

3OzmfP.png

思路如此清奇,拢共也就修改了10来个文件,不多不多,再来康康修改文件清单,也许你的 SystemUI 是在 framework目录下的,没关系代码也是一样的,继续看就是了。

	modified:   vendor/mediatek/proprietary/packages/apps/SystemUI/AndroidManifest.xml
	modified:   vendor/mediatek/proprietary/packages/apps/SystemUI/res/values-zh-rCN/strings.xml
	modified:   vendor/mediatek/proprietary/packages/apps/SystemUI/res/values/config.xml
	modified:   vendor/mediatek/proprietary/packages/apps/SystemUI/res/values/strings.xml
	modified:   vendor/mediatek/proprietary/packages/apps/SystemUI/src/com/android/systemui/SystemUIApplication.java
	modified:   vendor/mediatek/proprietary/packages/apps/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
	modified:   vendor/mediatek/proprietary/packages/apps/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
	modified:   vendor/mediatek/proprietary/packages/apps/SystemUI/src/com/android/systemui/statusbar/phone/SharedConfig.java
	modified:   vendor/mediatek/proprietary/packages/apps/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
	add:	    vendor/mediatek/proprietary/packages/apps/SystemUI/res/drawable-xhdpi/ic_rs_on.png
	add:	    vendor/mediatek/proprietary/packages/apps/SystemUI/res/drawable/message_dialog_bg.xml
	add:	    vendor/mediatek/proprietary/packages/apps/SystemUI/res/drawable/oval_count_bg.xml
	add:	    vendor/mediatek/proprietary/packages/apps/SystemUI/res/layout/countdown_layout.xml
	add:	    vendor/mediatek/proprietary/packages/apps/SystemUI/res/layout/stoprecordtip_layout.xml
	add:	    vendor/mediatek/proprietary/packages/apps/SystemUI/res/xml/file_paths.xml
	add:	    vendor/mediatek/proprietary/packages/apps/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
	add:	    vendor/mediatek/proprietary/packages/apps/SystemUI/src/com/android/systemui/screenrecord/

完整修改代码已经同步至 github

一、QSPanel 快捷控制开关

快捷开关添加可以参照 AirplaneModeTile.java 写法,

第一步、在 qs/tiles 路径下新增 ScreenRecordTile.java 处理快捷开关点击逻辑

第二步、在 res/values/config.xml 中增加 quick_settings_tiles_default 默认初始化项 recordscreen,
这个名字可以自己定,因为在接下来会判断是否 equals

第三步、在 qs/tileimpl/QSFactoryImpl.java 中新增 else if 条件实例化 ScreenRecordTile

<!-- The default tiles to display in QuickSettings -->
<string name="quick_settings_tiles_default" translatable="false">
    wifi,bt,dnd,battery,cell,airplane,cast,recordscreen
</string>


else if (tileSpec.equals("nfc")) return new NfcTile(mHost);
else if (tileSpec.equals("recordscreen")) return new ScreenRecordTile(mHost);

ScreenRecordTile 中核心的两个方法,handleClick() 和 handleUpdateState(BooleanState state, Object arg)

点击 icon 时都会触发 handleClick,我们就在这调用开始/结束录屏的方法,并保存一个录屏状态 KEY_SCREEN_RECORDING bool 值,用于每次 Panel 收起后展开都会触发 handleUpdateState 刷新 icon 状态,根据 bool 确定当前图标是否需要显示切割切线。

 @Override
    public void handleClick() {
        Log.d(TAG, "handleClick, RecordScreen enable = " + mState.value);
        
        boolean newState = !mState.value;
        refreshState(newState);
}

private boolean getRecordStatus(){
        return SharedConfig.getInstance(mContext).readBoolean(SharedConfig.KEY_SCREEN_RECORDING, false);
}

@Override
    protected void handleUpdateState(BooleanState state, Object arg) {
        Log.i(TAG, "handleUpdateState arg = " + arg);
        final boolean running = arg instanceof Boolean ? (Boolean)arg : getRecordStatus();
        state.value = running;
        state.label = mContext.getString(R.string.quick_settings_screenrecord_label);
        state.icon = ResourceIcon.get(R.drawable.ic_rs_on);
        if (state.slash == null) {
            state.slash = new SlashState();
        }
        state.slash.isSlashed = !running;
        state.state = running ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
        state.contentDescription = state.label;
        state.expandedAccessibilityClassName = Switch.class.getName();
    }

二、开始录制

界面显示部分暂时先这样,接下来就得来真格的了。通过后台 Service 监听开始/结束广播,这样既可以给 QSPanel 调用,也可作为接口提供给客户调用。(嘿嘿,没想到吧,这思路清奇不) 那么应该何时候启动 Service 呢,我们都知道 SystemUI 启动是非常早的,早在 Launcher 出现前,基本还处在开机动画阶段就已经启动完成,首先我们能想到的就是开机广播,在项目中全局搜索找到 SystemUIApplication.java,在 onCreat() 中监听了开机广播,那我们就加在这吧。

vendor\mediatek\proprietary\packages\apps\SystemUI\src\com\android\systemui\SystemUIApplication.java

    @Override
    public void onCreate() {
        super.onCreate();
        setTheme(R.style.Theme_SystemUI);

        SystemUIFactory.createFromConfig(this);

        if (Process.myUserHandle().equals(UserHandle.SYSTEM)) {
            IntentFilter filter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED);
            filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
            registerReceiver(new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (mBootCompleted) return;

                    //cczheng add for ScreenRecordService
                    Log.d("ScreenRecordAP", "BOOT_COMPLETED received,start RecordService");
                    startService(new Intent(SystemUIApplication.this, com.android.systemui.screenrecord.RecordService.class));
                    if (DEBUG) Log.v(TAG, "BOOT_COMPLETED received");
                    unregisterReceiver(this);
                    mBootCompleted = true;
                    

同时别忘记在 AndroidManifest.xml 中进行配置 RecordService

1、权限相关

1.1、录屏动态权限处理

录屏动态权限申请是啥呢?喏,就是下面这货

3OzZFI.png

正常流程通过 projectionManager.createScreenCaptureIntent(),在 onActivityResult() 接收授权结果,授权成功获取 data 中的 IBinder 对象从而得到 MediaProjection。

frameworks\base\media\java\android\media\projection\MediaProjectionManager.java

    /**
     * Returns an Intent that <b>must</b> passed to startActivityForResult()
     * in order to start screen capture. The activity will prompt
     * the user whether to allow screen capture.  The result of this
     * activity should be passed to getMediaProjection.
     */
    public Intent createScreenCaptureIntent() {
        Intent i = new Intent();
        i.setClassName("com.android.systemui",
                "com.android.systemui.media.MediaProjectionPermissionActivity");
        return i;
    }


	public MediaProjection getMediaProjection(int resultCode, @NonNull Intent resultData) {
        if (resultCode != Activity.RESULT_OK || resultData == null) {
            return null;
        }
        IBinder projection = resultData.getIBinderExtra(EXTRA_MEDIA_PROJECTION);
        if (projection == null) {
            return null;
        }
        return new MediaProjection(mContext, IMediaProjection.Stub.asInterface(projection));
    }

通过 Layout Inspetor 工具或者看上面的 createScreenCaptureIntent() 源码发现这货就在 SystemUI 中,对应 MediaProjectionPermissionActivity,只是设置了 Dialog 的主题,那我们就来康康它的源码,简化授权过程。

vendor\mediatek\proprietary\packages\apps\SystemUI\src\com\android\systemui\media\MediaProjectionPermissionActivity.java


        IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
        mService = IMediaProjectionManager.Stub.asInterface(b);

	try {
            if (mService.hasProjectionPermission(mUid, mPackageName)) {
                setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName,
                        false /*permanentGrant*/));
                finish();
                return;
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Error checking projection permissions", e);
            finish();
            return;
        }

	@Override
    public void onClick(DialogInterface dialog, int which) {
        try {
            if (which == AlertDialog.BUTTON_POSITIVE) {
                setResult(RESULT_OK, getMediaProjectionIntent(
                        mUid, mPackageName, mPermanentGrant));
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Error granting projection permission", e);
            setResult(RESULT_CANCELED);
        } finally {
            if (mDialog != null) {
                mDialog.dismiss();
            }
            finish();
        }
    }

	private Intent getMediaProjectionIntent(int uid, String packageName, boolean permanentGrant)
	            throws RemoteException {
	        IMediaProjection projection = mService.createProjection(uid, packageName,
	                 MediaProjectionManager.TYPE_SCREEN_CAPTURE, permanentGrant);
	        Intent intent = new Intent();
	        intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder());
	        return intent;
	}

可以看到 setResult() 对应的 Intent 都来自 getMediaProjectionIntent(),这下找到源头了,既然我们本身就在源码里修改那
就别绕弯子,拿来就用。所以综合上面的两块源码最终获取 MediaProjection 对象是介个亚子的

	private void initMediaProjection(){
        try {
            IBinder b = ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE);
            IMediaProjectionManager mService = IMediaProjectionManager.Stub.asInterface(b);
            String mPackageName = "com.android.systemui";
            ApplicationInfo aInfo = getPackageManager().getApplicationInfo(mPackageName, 0);
            IMediaProjection projection = mService.createProjection(aInfo.uid, mPackageName,
                 MediaProjectionManager.TYPE_SCREEN_CAPTURE, true);
            mediaProjection = new MediaProjection(RecordService.this, 
                  IMediaProjection.Stub.asInterface(projection.asBinder()));
        } catch (Exception e) {
            Log.e(TAG, "initMediaProjection happen some Exception", e);
            e.printStackTrace();
        }
        //projectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        //mediaProjection = mProjectionManager.getMediaProjection(mResultCode, mResultData);
    }

1.2、录音动态权限处理

mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);

录屏的同时也需要采集 MIC 的声音,所以需要在 AndroidManifest.xml 中声明权限

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

采用之前 Android9.0/8.1/6.0 默认给系统 app 授予所有权限 方案,在 pms_sysapp_grant_permission_list.txt 添加包名 com.android.systemui 即可

2、显示相关

2.1、3秒倒计时动画(缩放+透明)

通过 WindowManager 添加类型为 TYPE_APPLICATION_OVERLAY 的窗体

private void showCountDownWindow() {
        if (isShowCountDownView || inflateWindow != null || running) return;

        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        layoutParams.format = PixelFormat.RGBA_8888;
        layoutParams.gravity = Gravity.CENTER;
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        layoutParams.width = 500;
        layoutParams.height = 500;

        inflateWindow = LayoutInflater.from(RecordService.this).inflate(R.layout.countdown_layout, null);
        TextView animNumberTv = (TextView) inflateWindow.findViewById(R.id.tv_number_anim);

        mWindowManager.addView(inflateWindow, layoutParams);
        isShowCountDownView = true;

        doCountDownAnim(animNumberTv);
    }

    View inflateWindow;
    boolean isShowCountDownView;
    int sCurCount = 3;
    int repeatCount = 2;
    private void doCountDownAnim(final TextView animationViewTv){
        animationViewTv.setText(String.valueOf(sCurCount));
        animationViewTv.setVisibility(View.VISIBLE);

        AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0);
        ScaleAnimation scaleAnimation = new ScaleAnimation(
                0.1f, 1.3f, 0.1f, 1.3f,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);

        scaleAnimation.setRepeatCount(repeatCount);
        alphaAnimation.setRepeatCount(repeatCount);
        alphaAnimation.setDuration(1000);
        scaleAnimation.setDuration(1000);
        scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                animationViewTv.setVisibility(View.GONE);
                startRecord();
                sCurCount = 3;
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
                animationViewTv.setText(String.valueOf(--sCurCount));
            }
        });

        AnimationSet animationSet = new AnimationSet(true);
        animationSet.addAnimation(alphaAnimation);
        animationSet.addAnimation(scaleAnimation);
        animationViewTv.startAnimation(animationSet);
    }

2.2、statusBar背景修改为红色,表示正在录屏中

在 Activity 中通过 getWindow().setStatusBarColor(Color.RED); 就能修改 statusBar 背景色为空色,但这仅仅只属于顶部窗体,当你切换其它app时,将不再显示红色。我们的需求是只要在录屏中,不管如何操作,statusBar 就得保持红色。这是个问题?
经过布局文件等一顿分析后,在 StatusBar.java 中通过 mStatusBarView 可达到我们想要的效果。

private static final String OP_STATUSBAR_COLOR = "com.android.action.SET_STATUSBAR_COLOR";

 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (DEBUG) Log.v(TAG, "onReceive: " + intent);
            String action = intent.getAction();
            if (Intent.ACTION_SCREEN_OFF.equals(action)) {
                finishBarAnimations();
                resetUserExpandedStates();
            }else if (OP_STATUSBAR_COLOR.equals(action)) {
                Log.e("StatusBar", "OP_STATUSBAR_COLOR red: ");
                if(intent.hasExtra("is_recording")){
                    boolean result = intent.getBooleanExtra("is_recording", false);
                    if (mStatusBarView != null) 
                        mStatusBarView.setBackgroundColor(result 
                            ? android.graphics.Color.RED : android.graphics.Color.TRANSPARENT);
                }
            }

2.3、statusBar 点击弹出 IOS 风格对话框是否停止录制

IOS 风格对话框网上一大推,随便征用一个吧。还记得上面录屏权限那货吧,这里我们仿照它使用 Dialog 主题,便于在 PanelView 通过 startActivity 方式展示对话框。那么,PanelView 是怎么出来的呢?

89erAH.jpg

只能说熟悉 StatusBar 的盆友们一看就知道,不知道的我给你说下,反手就是一个三连。

PhoneStatusBarView.java PanelBar.java PanelView.java

简单来说,PhoneStatusBarView 对应顶部的状态栏,PanelBar 对应状态栏下拉面板,PanelView 对应面板具体内容

触摸事件就是按照上面的顺序来传递的,以往我们屏蔽下拉最简单办法就在 PhoneStatusBarView 拦截,但现在我们得到事件处理的最底端

PanelView 中

vendor\mediatek\proprietary\packages\apps\SystemUI\src\com\android\systemui\statusbar\phone\PanelView.java

boolean justClick;

@Override
public boolean onTouchEvent(MotionEvent event) {

		boolean isRecording = SharedConfig.getInstance(mContext).readBoolean(SharedConfig.KEY_SCREEN_RECORDING, false);

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                //cczheng 
                if (!isRecording) {
                    startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
                }
				justClick = true;
                log("ACTION_DOWN");

				.....
                break;
				
				......
			case MotionEvent.ACTION_MOVE:
				....
			 	justClick = false;
                android.util.Log.d("cpanel","ACTION_MOVE");
                break;
			case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (justClick && isRecording) {
                    log("showdialog");
                    mContext.startActivity(new android.content.Intent("com.android.systemui.stoprecrodtipactivity")
                            .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK));
                }
                log("ACTION_CANCEL =="+justClick);
                trackMovement(event);
                endMotionEvent(event, x, y, false /* forceCancel */);
                break;
        }
        return !mGestureWaitForTouchSlop || mTracking;
    }

正常情况下,快速点击抬起手指,PanelView 会有个向下伸张回弹的动画,很显然在录屏模式下我们要屏蔽这个操作,

case MotionEvent.ACTION_DOWN isRecording 情况下不走 startExpandMotion() 即可

case MotionEvent.ACTION_MOVE justClick = false,有拖动则正常下拉

case MotionEvent.ACTION_UP justClick && isRecording 弹出是否停止录屏对话框

三、结束录制

1、显示相关

3.1、statusBar 恢复默认背景

private void setStatusBarColor(){
    sendBroadcast(new Intent("com.android.action.SET_STATUSBAR_COLOR").putExtra("is_recording", false));
}

3.2、悬挂通知刚刚录制成功的视频,更新媒体库数据

需要注意的几个地方,悬挂通知类型需要设置为 NotificationManager.IMPORTANCE_HIGH,不然不能正常显示

7.0 以上访问文件uri时需要通过 FileProvider 来访问,并在 AndroidManif.xml 中配置,不然会报 FileUriExposedException异常

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.android.systemui.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

private void showHeadUpNotification(){
        final NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        String channelId = java.util.UUID.randomUUID().toString().replaceAll("-", "");
        NotificationChannel notificationChannel = new NotificationChannel(channelId,
                "recordScreen", NotificationManager.IMPORTANCE_HIGH);
        notificationChannel.setSound(null, null);//mute
        mNotificationManager.createNotificationChannel(notificationChannel);

        Notification.Builder builder = new Notification.Builder(this, channelId);
        builder.setSmallIcon(R.drawable.ic_rs_on);
        builder.setSubText(getResources().getString(R.string.record_notification_subtext));
        builder.setAutoCancel(true);
        builder.setContentText(getResources().getString(R.string.record_notification_contentext));

        //for goto gallery
        Intent intent = new Intent(Intent.ACTION_VIEW);
        File recordFile = new File(recordFilePath);
        Uri uriForFile = FileProvider.getUriForFile(this,"com.android.systemui.fileprovider", recordFile);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.setDataAndType(uriForFile, "video/*");

        //scan media file to gallery
        MediaScannerConnection.scanFile(this, new String[]{recordFile.getAbsolutePath()},
                new String[]{"video/*"}, null);

        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
        builder.setContentIntent(pendingIntent);
        builder.setFullScreenIntent(pendingIntent, true);
        final int notifyId = 100;
        mNotificationManager.notify(notifyId, builder.build());

        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                mNotificationManager.cancel(notifyId);
            }
        },4500);

    }

3.3、点击通知跳转至系统媒体库自动播放

		//scan media file to gallery
        Intent intent = new Intent(Intent.ACTION_VIEW);
        File recordFile = new File(recordFilePath);
        Uri uriForFile = FileProvider.getUriForFile(this,"com.android.systemui.fileprovider", recordFile);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.setDataAndType(uriForFile, "video/*");
发布了87 篇原创文章 · 获赞 157 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/u012932409/article/details/104779585