zoukankan      html  css  js  c++  java
  • Android8.1 MTK平台 截屏功能分析

    前言

    涉及到的源码有

    frameworksaseservicescorejavacomandroidserverpolicyPhoneWindowManager.java

    vendormediatekproprietarypackagesappsSystemUIsrccomandroidsystemuiscreenshotTakeScreenshotService.java
    vendormediatekproprietarypackagesappsSystemUIsrccomandroidsystemuiscreenshotGlobalScreenshot.java

    按键处理都是在 PhoneWindowManager 中,真正截屏的功能实现在 GlobalScreenshot 中, PhoneWindowManager 和 systemui 通过 bind TakeScreenshotService 来实现截屏功能

    流程

    一般未经过特殊定制的 Android 系统,截屏都是通过同时按住音量下键和电源键来截屏,后来我们使用的一些华为、oppo等厂商的系统你会发现可以通过三指滑动来截屏,下一篇我们会定制此功能,而且截屏显示风格类似 iphone 在左下角显示截屏缩略图,点击可跳转放大查看,3s 无操作后向左自动滑动消失。

    好了,现在我们先来理一下系统截屏的流程

    	system_process D/WindowManager: interceptKeyTi keyCode=25 down=true repeatCount=0 keyguardOn=false mHomePressed=false canceled=false metaState:0
    	system_process D/WindowManager: interceptKeyTq keycode=25 interactive=true keyguardActive=false policyFlags=22000000 down =false canceled = false isWakeKey=false mVolumeDownKeyTriggered =true result = 1 useHapticFeedback = false isInjected = false
    	system_process D/WindowManager: interceptKeyTi keyCode=25 down=false repeatCount=0 keyguardOn=false mHomePressed=false canceled=false metaState:0
    	system_process D/WindowManager: interceptKeyTq keycode=26 interactive=true keyguardActive=false policyFlags=22000000 down =false canceled = false isWakeKey=false mVolumeDownKeyTriggered =false result = 1 useHapticFeedback = false isInjected = false
    
    

    上面是按下音量下键和电源键的日志,音量下键对应 keyCode=25 ,电源键对应 keyCode=26,来看到 PhoneWindowManager 中的 interceptKeyBeforeQueueing() 方法,在此处处理按键操作

    	/** {@inheritDoc} */
        @Override
        public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
            if (!mSystemBooted) {
                // If we have not yet booted, don't let key events do anything.
                return 0;
            }
    
          	.....
    
            if (DEBUG_INPUT) {
                Log.d(TAG, "interceptKeyTq keycode=" + keyCode
                        + " interactive=" + interactive + " keyguardActive=" + keyguardActive
                        + " policyFlags=" + Integer.toHexString(policyFlags));
            }
    
          .....
    
            // Handle special keys.
            switch (keyCode) {
                .......
    
                case KeyEvent.KEYCODE_VOLUME_DOWN:
                case KeyEvent.KEYCODE_VOLUME_UP:
                case KeyEvent.KEYCODE_VOLUME_MUTE: {
                    if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
                        if (down) {
                            if (interactive && !mScreenshotChordVolumeDownKeyTriggered
                                    && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
                                mScreenshotChordVolumeDownKeyTriggered = true;
                                mScreenshotChordVolumeDownKeyTime = event.getDownTime();
                                mScreenshotChordVolumeDownKeyConsumed = false;
                                cancelPendingPowerKeyAction();
                                interceptScreenshotChord();
                                interceptAccessibilityShortcutChord();
                            }
                        } else {
                            mScreenshotChordVolumeDownKeyTriggered = false;
                            cancelPendingScreenshotChordAction();
                            cancelPendingAccessibilityShortcutAction();
                        }
                    } 
    		....
    	}
    

    看到 KEYCODE_VOLUME_DOWN 中,记录当前按下音量下键的时间 mScreenshotChordVolumeDownKeyTime,cancelPendingPowerKeyAction() 移除电源键长按消息 MSG_POWER_LONG_PRESS,来看下核心方法 interceptScreenshotChord()

    // Time to volume and power must be pressed within this interval of each other.
    private static final long SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS = 150;
    
    private void interceptScreenshotChord() {
            if (mScreenshotChordEnabled
                    && mScreenshotChordVolumeDownKeyTriggered && mScreenshotChordPowerKeyTriggered
                    && !mA11yShortcutChordVolumeUpKeyTriggered) {
                final long now = SystemClock.uptimeMillis();
                if (now <= mScreenshotChordVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS
                        && now <= mScreenshotChordPowerKeyTime
                                + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) {
                    mScreenshotChordVolumeDownKeyConsumed = true;
                    cancelPendingPowerKeyAction();
                    mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
                    mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay());
                }
            }
        }
    

    只有当电源键按下时 mScreenshotChordPowerKeyTriggered 才为 true, 当两个按键的按下时间都大于 150 时,延时执行截屏任务 mScreenshotRunnable

    private long getScreenshotChordLongPressDelay() {
            if (mKeyguardDelegate.isShowing()) {
                // Double the time it takes to take a screenshot from the keyguard
                return (long) (KEYGUARD_SCREENSHOT_CHORD_DELAY_MULTIPLIER *
                        ViewConfiguration.get(mContext).getDeviceGlobalActionKeyTimeout());
            }
            return ViewConfiguration.get(mContext).getDeviceGlobalActionKeyTimeout();
        }
    

    若当前输入框是打开状态,则延时时间为输入框关闭时间加上系统配置的按键超时时间,若当前输入框没有打开则直接是系统配置的按键超时处理时间

    紧接着看下 mScreenshotRunnable 都做了什么操作

    private class ScreenshotRunnable implements Runnable {
            private int mScreenshotType = TAKE_SCREENSHOT_FULLSCREEN;
    
            public void setScreenshotType(int screenshotType) {
                mScreenshotType = screenshotType;
            }
    
            @Override
            public void run() {
                takeScreenshot(mScreenshotType);
            }
        }
    
    private final ScreenshotRunnable mScreenshotRunnable = new ScreenshotRunnable();
    

    可以看到在线程中调用了 takeScreenshot(),默认不设置截屏类型就是全屏,截屏类型有 TAKE_SCREENSHOT_SELECTED_REGION 选定的区域 和 TAKE_SCREENSHOT_FULLSCREEN 全屏两种类型

    // Assume this is called from the Handler thread.
        private void takeScreenshot(final int screenshotType) {
            synchronized (mScreenshotLock) {
                if (mScreenshotConnection != null) {
                    return;
                }
                final ComponentName serviceComponent = new ComponentName(SYSUI_PACKAGE,
                        SYSUI_SCREENSHOT_SERVICE);
                final Intent serviceIntent = new Intent();
                serviceIntent.setComponent(serviceComponent);
                ServiceConnection conn = new ServiceConnection() {
                    @Override
                    public void onServiceConnected(ComponentName name, IBinder service) {
                        synchronized (mScreenshotLock) {
                            if (mScreenshotConnection != this) {
                                return;
                            }
                            Messenger messenger = new Messenger(service);
                            Message msg = Message.obtain(null, screenshotType);
                            final ServiceConnection myConn = this;
                            Handler h = new Handler(mHandler.getLooper()) {
                                @Override
                                public void handleMessage(Message msg) {
                                    synchronized (mScreenshotLock) {
                                        if (mScreenshotConnection == myConn) {
                                            mContext.unbindService(mScreenshotConnection);
                                            mScreenshotConnection = null;
                                            mHandler.removeCallbacks(mScreenshotTimeout);
                                        }
                                    }
                                }
                            };
                            msg.replyTo = new Messenger(h);
                            msg.arg1 = msg.arg2 = 0;
                            if (mStatusBar != null && mStatusBar.isVisibleLw())
                                msg.arg1 = 1;
                            if (mNavigationBar != null && mNavigationBar.isVisibleLw())
                                msg.arg2 = 1;
                            try {
                                messenger.send(msg);
                            } catch (RemoteException e) {
                            }
                        }
                    }
    
                    @Override
                    public void onServiceDisconnected(ComponentName name) {
                        synchronized (mScreenshotLock) {
                            if (mScreenshotConnection != null) {
                                mContext.unbindService(mScreenshotConnection);
                                mScreenshotConnection = null;
                                mHandler.removeCallbacks(mScreenshotTimeout);
                                notifyScreenshotError();
                            }
                        }
                    }
                };
                if (mContext.bindServiceAsUser(serviceIntent, conn,
                        Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
                        UserHandle.CURRENT)) {
                    mScreenshotConnection = conn;
                    mHandler.postDelayed(mScreenshotTimeout, 10000);
                }
            }
        }
    

    takeScreenshot 中通过 bind SystemUI中的 TakeScreenshotService 建立连接,连接成功后通过 Messenger 在两个进程中传递消息通行,有点类似 AIDL,关于 Messenger 的介绍可参考 Android进程间通讯之 messenger Messenger 主要传递当前的 mStatusBar 和 mNavigationBar 是否可见,再来看 TakeScreenshotService 中如何接收处理

    public class TakeScreenshotService extends Service {
        private static final String TAG = "TakeScreenshotService";
    
        private static GlobalScreenshot mScreenshot;
    
        private Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                final Messenger callback = msg.replyTo;
                Runnable finisher = new Runnable() {
                    @Override
                    public void run() {
                        Message reply = Message.obtain(null, 1);
                        try {
                            callback.send(reply);
                        } catch (RemoteException e) {
                        }
                    }
                };
    
                // If the storage for this user is locked, we have no place to store
                // the screenshot, so skip taking it instead of showing a misleading
                // animation and error notification.
                if (!getSystemService(UserManager.class).isUserUnlocked()) {
                    Log.w(TAG, "Skipping screenshot because storage is locked!");
                    post(finisher);
                    return;
                }
    
                if (mScreenshot == null) {
                    mScreenshot = new GlobalScreenshot(TakeScreenshotService.this);
                }
    
                switch (msg.what) {
                    case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
                        mScreenshot.takeScreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0);
                        break;
                    case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION:
                        mScreenshot.takeScreenshotPartial(finisher, msg.arg1 > 0, msg.arg2 > 0);
                        break;
                }
            }
        };
    
        @Override
        public IBinder onBind(Intent intent) {
            return new Messenger(mHandler).getBinder();
        }
    
        @Override
        public boolean onUnbind(Intent intent) {
            if (mScreenshot != null) mScreenshot.stopScreenshot();
            return true;
        }
    }
    

    可以看到通过 mHandler 接收传递的消息,获取截屏类型和是否要包含状态栏、导航栏,通过创建 GlobalScreenshot 对象(真正干活的来了),调用 takeScreenshot 执行截屏操作,继续跟进

    	void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) {
            mDisplay.getRealMetrics(mDisplayMetrics);
            takeScreenshot(finisher, statusBarVisible, navBarVisible, 0, 0, mDisplayMetrics.widthPixels,
                    mDisplayMetrics.heightPixels);
        }
    
        /**
         * Takes a screenshot of the current display and shows an animation.
         */
        void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible,
                int x, int y, int width, int height) {
            // We need to orient the screenshot correctly (and the Surface api seems to take screenshots
            // only in the natural orientation of the device :!)
            mDisplay.getRealMetrics(mDisplayMetrics);
            float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels};
            float degrees = getDegreesForRotation(mDisplay.getRotation());
            boolean requiresRotation = (degrees > 0);
            if (requiresRotation) {
                // Get the dimensions of the device in its native orientation
                mDisplayMatrix.reset();
                mDisplayMatrix.preRotate(-degrees);
                mDisplayMatrix.mapPoints(dims);
                dims[0] = Math.abs(dims[0]);
                dims[1] = Math.abs(dims[1]);
            }
    
            // Take the screenshot
            mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
            if (mScreenBitmap == null) {
                notifyScreenshotError(mContext, mNotificationManager,
                        R.string.screenshot_failed_to_capture_text);
                finisher.run();
                return;
            }
    
            if (requiresRotation) {
                // Rotate the screenshot to the current orientation
                Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
                        mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888,
                        mScreenBitmap.hasAlpha(), mScreenBitmap.getColorSpace());
                Canvas c = new Canvas(ss);
                c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
                c.rotate(degrees);
                c.translate(-dims[0] / 2, -dims[1] / 2);
                c.drawBitmap(mScreenBitmap, 0, 0, null);
                c.setBitmap(null);
                // Recycle the previous bitmap
                mScreenBitmap.recycle();
                mScreenBitmap = ss;
            }
    
            if (width != mDisplayMetrics.widthPixels || height != mDisplayMetrics.heightPixels) {
                // Crop the screenshot to selected region
                Bitmap cropped = Bitmap.createBitmap(mScreenBitmap, x, y, width, height);
                mScreenBitmap.recycle();
                mScreenBitmap = cropped;
            }
    
            // Optimizations
            mScreenBitmap.setHasAlpha(false);
            mScreenBitmap.prepareToDraw();
    
            // Start the post-screenshot animation
            startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,
                    statusBarVisible, navBarVisible);
        }
    

    获取屏幕的宽高和当前屏幕方向以确定是否需要旋转图片,然后通过 SurfaceControl.screenshot 截屏,好吧,再继续往下看到

    public static Bitmap screenshot(int width, int height) {
        // TODO: should take the display as a parameter
        IBinder displayToken = SurfaceControl.getBuiltInDisplay(
                SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN);
        return nativeScreenshot(displayToken, new Rect(), width, height, 0, 0, true,
                false, Surface.ROTATION_0);
    }
    

    这里调用的是 nativeScreenshot 方法,它是一个 native 方法,具体的实现在JNI层,这里就不做过多的介绍了。继续回到我们的 takeScreenshot 方法,在调用了截屏方法 screentshot 之后,判断是否截屏成功:
    截屏失败则调用 notifyScreenshotError 发送通知。截屏成功,则调用 startAnimation 播放动画,来分析下动画,后面我们会改这个动画的效果

    	/**
         * Starts the animation after taking the screenshot
         */
        private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible,
                boolean navBarVisible) {
            // If power save is on, show a toast so there is some visual indication that a screenshot
            // has been taken.
            PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
            if (powerManager.isPowerSaveMode()) {
                Toast.makeText(mContext, R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show();
            }
    
            // Add the view for the animation
            mScreenshotView.setImageBitmap(mScreenBitmap);
            mScreenshotLayout.requestFocus();
    
            // Setup the animation with the screenshot just taken
            if (mScreenshotAnimation != null) {
                if (mScreenshotAnimation.isStarted()) {
                    mScreenshotAnimation.end();
                }
                mScreenshotAnimation.removeAllListeners();
            }
    
            mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
            ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation();
            ValueAnimator screenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h,
                    statusBarVisible, navBarVisible);
            mScreenshotAnimation = new AnimatorSet();
            mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim);
            mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    // Save the screenshot once we have a bit of time now
                    saveScreenshotInWorkerThread(finisher);
                    mWindowManager.removeView(mScreenshotLayout);
    
                    // Clear any references to the bitmap
                    mScreenBitmap = null;
                    mScreenshotView.setImageBitmap(null);
                }
            });
            mScreenshotLayout.post(new Runnable() {
                @Override
                public void run() {
                    // Play the shutter sound to notify that we've taken a screenshot
                    mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
    
                    mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
                    mScreenshotView.buildLayer();
                    mScreenshotAnimation.start();
                }
            });
        }
    

    先判断是否是低电量模式,若是发出已抓取屏幕截图的 toast,然后通过 WindowManager 在屏幕中间添加一个装有截屏缩略图的 view,同时创建两个动画组合,通过 mCameraSound 播放截屏咔嚓声并执行动画,动画结束后移除刚刚添加的 view,同时调用 saveScreenshotInWorkerThread 保存图片到媒体库,我们直接来看 SaveImageInBackgroundTask

    class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
        .....
    
        SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data,
                NotificationManager nManager) {
           ......
    
            mNotificationBuilder = new Notification.Builder(context, NotificationChannels.SCREENSHOTS)
                .setTicker(r.getString(R.string.screenshot_saving_ticker)
                        + (mTickerAddSpace ? " " : ""))
                .setContentTitle(r.getString(R.string.screenshot_saving_title))
                .setContentText(r.getString(R.string.screenshot_saving_text))
                .setSmallIcon(R.drawable.stat_notify_image)
                .setWhen(now)
                .setShowWhen(true)
                .setColor(r.getColor(com.android.internal.R.color.system_notification_accent_color))
                .setStyle(mNotificationStyle)
                .setPublicVersion(mPublicNotificationBuilder.build());
            mNotificationBuilder.setFlag(Notification.FLAG_NO_CLEAR, true);
            SystemUI.overrideNotificationAppName(context, mNotificationBuilder);
    
            mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT,
                    mNotificationBuilder.build());
        }
    
        @Override
        protected Void doInBackground(Void... params) {
            if (isCancelled()) {
                return null;
            }
    
            // By default, AsyncTask sets the worker thread to have background thread priority, so bump
            // it back up so that we save a little quicker.
            Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
    
            Context context = mParams.context;
            Bitmap image = mParams.image;
            Resources r = context.getResources();
    
            try {
                // Create screenshot directory if it doesn't exist
                mScreenshotDir.mkdirs();
    
                // media provider uses seconds for DATE_MODIFIED and DATE_ADDED, but milliseconds
                // for DATE_TAKEN
                long dateSeconds = mImageTime / 1000;
    
                // Save
                OutputStream out = new FileOutputStream(mImageFilePath);
                image.compress(Bitmap.CompressFormat.PNG, 100, out);
                out.flush();
                out.close();
    
                // Save the screenshot to the MediaStore
                ContentValues values = new ContentValues();
                ContentResolver resolver = context.getContentResolver();
                values.put(MediaStore.Images.ImageColumns.DATA, mImageFilePath);
                values.put(MediaStore.Images.ImageColumns.TITLE, mImageFileName);
                values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, mImageFileName);
                values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, mImageTime);
                values.put(MediaStore.Images.ImageColumns.DATE_ADDED, dateSeconds);
                values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, dateSeconds);
                values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png");
                values.put(MediaStore.Images.ImageColumns.WIDTH, mImageWidth);
                values.put(MediaStore.Images.ImageColumns.HEIGHT, mImageHeight);
                values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length());
                Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    
                // Create a share intent
                String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
                String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
                Intent sharingIntent = new Intent(Intent.ACTION_SEND);
                sharingIntent.setType("image/png");
                sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
                sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
    
                // Create a share action for the notification. Note, we proxy the call to ShareReceiver
                // because RemoteViews currently forces an activity options on the PendingIntent being
                // launched, and since we don't want to trigger the share sheet in this case, we will
                // start the chooser activitiy directly in ShareReceiver.
                PendingIntent shareAction = PendingIntent.getBroadcast(context, 0,
                        new Intent(context, GlobalScreenshot.ShareReceiver.class)
                                .putExtra(SHARING_INTENT, sharingIntent),
                        PendingIntent.FLAG_CANCEL_CURRENT);
                Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
                        R.drawable.ic_screenshot_share,
                        r.getString(com.android.internal.R.string.share), shareAction);
                mNotificationBuilder.addAction(shareActionBuilder.build());
    
                // Create a delete action for the notification
                PendingIntent deleteAction = PendingIntent.getBroadcast(context, 0,
                        new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
                                .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()),
                        PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
                Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
                        R.drawable.ic_screenshot_delete,
                        r.getString(com.android.internal.R.string.delete), deleteAction);
                mNotificationBuilder.addAction(deleteActionBuilder.build());
    
                mParams.imageUri = uri;
                mParams.image = null;
                mParams.errorMsgResId = 0;
            } catch (Exception e) {
                // IOException/UnsupportedOperationException may be thrown if external storage is not
                // mounted
                Slog.e(TAG, "unable to save screenshot", e);
                mParams.clearImage();
                mParams.errorMsgResId = R.string.screenshot_failed_to_save_text;
            }
    
            // Recycle the bitmap data
            if (image != null) {
                image.recycle();
            }
    
            return null;
        }
    
        @Override
        protected void onPostExecute(Void params) {
            if (mParams.errorMsgResId != 0) {
                // Show a message that we've failed to save the image to disk
                GlobalScreenshot.notifyScreenshotError(mParams.context, mNotificationManager,
                        mParams.errorMsgResId);
            } else {
                // Show the final notification to indicate screenshot saved
                Context context = mParams.context;
                Resources r = context.getResources();
    
                // Create the intent to show the screenshot in gallery
                Intent launchIntent = new Intent(Intent.ACTION_VIEW);
                launchIntent.setDataAndType(mParams.imageUri, "image/png");
                launchIntent.setFlags(
                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
    
                final long now = System.currentTimeMillis();
    
                // Update the text and the icon for the existing notification
                mPublicNotificationBuilder
                        .setContentTitle(r.getString(R.string.screenshot_saved_title))
                        .setContentText(r.getString(R.string.screenshot_saved_text))
                        .setContentIntent(PendingIntent.getActivity(mParams.context, 0, launchIntent, 0))
                        .setWhen(now)
                        .setAutoCancel(true)
                        .setColor(context.getColor(
                                com.android.internal.R.color.system_notification_accent_color));
                mNotificationBuilder
                    .setContentTitle(r.getString(R.string.screenshot_saved_title))
                    .setContentText(r.getString(R.string.screenshot_saved_text))
                    .setContentIntent(PendingIntent.getActivity(mParams.context, 0, launchIntent, 0))
                    .setWhen(now)
                    .setAutoCancel(true)
                    .setColor(context.getColor(
                            com.android.internal.R.color.system_notification_accent_color))
                    .setPublicVersion(mPublicNotificationBuilder.build())
                    .setFlag(Notification.FLAG_NO_CLEAR, false);
    
                mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT,
                        mNotificationBuilder.build());
            }
            mParams.finisher.run();
            mParams.clearContext();
        }
    
        @Override
        protected void onCancelled(Void params) {
            // If we are cancelled while the task is running in the background, we may get null params.
            // The finisher is expected to always be called back, so just use the baked-in params from
            // the ctor in any case.
            mParams.finisher.run();
            mParams.clearImage();
            mParams.clearContext();
    
            // Cancel the posted notification
            mNotificationManager.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT);
        }
    }
    

    简单说下, SaveImageInBackgroundTask 构造方法中做了大量的准备工作,截屏图片的时间命名格式、截屏通知对象创建,在 doInBackground 中将截屏图片通过 ContentResolver 存储至 MediaStore,再创建两个 PendingIntent,用于分享和删除截屏图片,在 onPostExecute 中发送刚刚创建的 Notification 至 statuBar 显示,到此截屏的流程就结束了。

    其它

    我们再回到 PhoneWindowManager 中看下,通过上面我们知道要想截屏只需通过如下两行代码即可

    mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
    mHandler.post(mScreenshotRunnable);
    

    通过搜索上面的关键代码,我们发现还有另外两处也调用了截屏的代码,一起来看下

    @Override
    public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags) {
        final boolean keyguardOn = keyguardOn();
        final int keyCode = event.getKeyCode();
    	.....
    	
     	else if (keyCode == KeyEvent.KEYCODE_S && event.isMetaPressed()
                    && event.isCtrlPressed()) {
                if (down && repeatCount == 0) {
                    int type = event.isShiftPressed() ? TAKE_SCREENSHOT_SELECTED_REGION
                            : TAKE_SCREENSHOT_FULLSCREEN;
                    mScreenshotRunnable.setScreenshotType(type);
                    mHandler.post(mScreenshotRunnable);
                    return -1;
                }
        }
    	....
    
    	else if (keyCode == KeyEvent.KEYCODE_SYSRQ) {
            if (down && repeatCount == 0) {
                mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
                mHandler.post(mScreenshotRunnable);
            }
            return -1;
        }
    	......
    
    }
    

    也是在拦截按键消息分发之前的方法中,查看 KeyEvent 源码,第一种情况大概网上搜索了下,应该是接外设时,同时按下 S 键 + Meta键 + Ctrl键即可截屏,关于 Meta 介绍可参考Meta键始末 第二种情况是按下截屏键时,对应 keyCode 为 120,可以用 adb shell input keyevent 120 模拟发现也能截屏

     /** Key code constant: 'S' key. */
        public static final int KEYCODE_S               = 47;
    
    /** Key code constant: System Request / Print Screen key. */
        public static final int KEYCODE_SYSRQ           = 120;
    
    

    常用按键对应值

    ebFZ5R.png

    这样文章开头提到的三指截屏操作,我们就可以加在 PhoneWindowManager 中,当手势监听获取到三指时,只需调用截屏的两行代码即可

    总结

    • 在 PhoneWindowManager 的 dispatchUnhandledKey 方法中处理App无法处理的按键事件,当然也包括音量减少键和电源按键的组合按键

    • 通过一系列的调用启动 TakeScreenshotService 服务,并通过其执行截屏的操作。

    • 具体的截屏代码是在 native 层实现的。

    • 截屏操作时候,若截屏失败则直接发送截屏失败的 notification 通知。

    • 截屏之后,若截屏成功,则先执行截屏的动画,并在动画效果执行完毕之后,发送截屏成功的 notification 的通知。

    参考文章

    Android 截屏方法总结
    Android KeyCode列表

  • 相关阅读:
    学到的一种编程风格
    防止忘记初始化NSMutableArray的方法
    == 和 isEqualToString的区别之备忘
    IOS开发中--点击imageView上的Button没有任何反应
    [__NSCFString absoluteURL]错误的解决方案
    二叉树镜像
    判断树的子结构
    算法练习-贪心问题
    重建二叉树:
    IDEA搭建web项目出现中文乱码问题
  • 原文地址:https://www.cnblogs.com/cczheng-666/p/11365869.html
Copyright © 2011-2022 走看看