欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android 截图功能源码的分析

程序员文章站 2023-12-19 13:32:40
android 截图功能源码的分析 一般没有修改rom的android原生系统截图功能的组合键是音量减+开机键;今天我们从源码角度来分析截图功能是如何在源码中实现的。...

android 截图功能源码的分析

一般没有修改rom的android原生系统截图功能的组合键是音量减+开机键;今天我们从源码角度来分析截图功能是如何在源码中实现的。

在android系统中,由于我们的每一个android界面都是一个activity,而界面的显示都是通过window对象实现的,每个window对象实际上都是phonewindow的实例,而每个phonewindow对象都对应一个phonewindowmanager对象,当我们在activity界面执行按键操作的时候,在将按键的处理操作分发到app之前,首先会回调phonewindowmanager中的dispatchunhandledkey方法,该方法主要用于执行当前app处理按键之前的操作,我们具体看一下该方法的实现。

/** {@inheritdoc} */
  @override
  public keyevent dispatchunhandledkey(windowstate win, keyevent event, int policyflags) {
    ...
    keyevent fallbackevent = null;
    if ((event.getflags() & keyevent.flag_fallback) == 0) {
      final keycharactermap kcm = event.getkeycharactermap();
      final int keycode = event.getkeycode();
      final int metastate = event.getmetastate();
      final boolean initialdown = event.getaction() == keyevent.action_down
          && event.getrepeatcount() == 0;

      // check for fallback actions specified by the key character map.
      final fallbackaction fallbackaction;
      if (initialdown) {
        fallbackaction = kcm.getfallbackaction(keycode, metastate);
      } else {
        fallbackaction = mfallbackactions.get(keycode);
      }

      if (fallbackaction != null) {
        ...
        final int flags = event.getflags() | keyevent.flag_fallback;
        fallbackevent = keyevent.obtain(
            event.getdowntime(), event.geteventtime(),
            event.getaction(), fallbackaction.keycode,
            event.getrepeatcount(), fallbackaction.metastate,
            event.getdeviceid(), event.getscancode(),
            flags, event.getsource(), null);

        if (!interceptfallback(win, fallbackevent, policyflags)) {
          fallbackevent.recycle();
          fallbackevent = null;
        }

        if (initialdown) {
          mfallbackactions.put(keycode, fallbackaction);
        } else if (event.getaction() == keyevent.action_up) {
          mfallbackactions.remove(keycode);
          fallbackaction.recycle();
        }
      }
    }

    ...
    return fallbackevent;
  }

这里我们关注一下方法体中调用的:interceptfallback方法,通过调用该方法将处理按键的操作下发到该方法中,我们继续看一下该方法的实现逻辑。

private boolean interceptfallback(windowstate win, keyevent fallbackevent, int policyflags) {
    int actions = interceptkeybeforequeueing(fallbackevent, policyflags);
    if ((actions & action_pass_to_user) != 0) {
      long delaymillis = interceptkeybeforedispatching(
          win, fallbackevent, policyflags);
      if (delaymillis == 0) {
        return true;
      }
    }
    return false;
  }

然后我们看到在interceptfallback方法中我们调用了interceptkeybeforequeueing方法,通过阅读我们我们知道该方法主要实现了对截屏按键的处理流程,这样我们继续看一下interceptkeybeforewueueing方法的处理:

@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;
    }

    ...
    // handle special keys.
    switch (keycode) {
      case keyevent.keycode_volume_down:
      case keyevent.keycode_volume_up:
      case keyevent.keycode_volume_mute: {
        if (musetvrouting) {
          // on tvs volume keys never go to the foreground app
          result &= ~action_pass_to_user;
        }
        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();
            }
          } else {
            mscreenshotchordvolumedownkeytriggered = false;
            cancelpendingscreenshotchordaction();
          }
        }
        ...

    return result;
  }

可以发现这里首先判断当前系统是否已经boot完毕,若尚未启动完毕,则所有的按键操作都将失效,若启动完成,则执行后续的操作,这里我们只是关注音量减少按键和电源按键组合的处理事件。另外这里多说一句想安卓系统的home按键事件,menu按键事件,进程列表按键事件等等都是在这里实现的,后续中我们会陆续介绍这方面的内容。

回到我们的interceptkeybeforequeueing方法,当我用按下音量减少按键的时候回进入到:case keyevent.keycode_volume_mute分支并执行相应的逻辑,然后同时判断用户是否按下了电源键,若同时按下了电源键,则执行:

if (interactive && !mscreenshotchordvolumedownkeytriggered
                && (event.getflags() & keyevent.flag_fallback) == 0) {
              mscreenshotchordvolumedownkeytriggered = true;
              mscreenshotchordvolumedownkeytime = event.getdowntime();
              mscreenshotchordvolumedownkeyconsumed = false;
              cancelpendingpowerkeyaction();
              interceptscreenshotchord();
            }

可以发现这里的interceptscreenshotchrod方法就是系统准备开始执行截屏操作的开始,我们继续看一下interceptcreenshotchord方法的实现。

private void interceptscreenshotchord() {
    if (mscreenshotchordenabled
        && mscreenshotchordvolumedownkeytriggered && mscreenshotchordpowerkeytriggered
        && !mscreenshotchordvolumeupkeytriggered) {
      final long now = systemclock.uptimemillis();
      if (now <= mscreenshotchordvolumedownkeytime + screenshot_chord_debounce_delay_millis
          && now <= mscreenshotchordpowerkeytime
              + screenshot_chord_debounce_delay_millis) {
        mscreenshotchordvolumedownkeyconsumed = true;
        cancelpendingpowerkeyaction();

        mhandler.postdelayed(mscreenshotrunnable, getscreenshotchordlongpressdelay());
      }
    }
  }

在方法体中我们最终会执行发送一个延迟的异步消息,请求执行截屏的操作而这里的延时时间,若当前输入框是打开状态,则延时时间为输入框关闭时间加上系统配置的按键超时时间,若当前输入框没有打开则直接是系统配置的按键超时处理时间,可看一下getscreenshotchordlongpressdelay方法的具体实现。

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();
  }

回到我们的interceptscreenshotchord方法,发送了异步消息之后系统最终会被我们发送的runnable对象的run方法执行;这样我们看一下runnable类型的mscreenshotrunnable的run方法的实现:

private final runnable mscreenshotrunnable = new runnable() {
    @override
    public void run() {
      takescreenshot();
    }
  };

好吧,方法体中并未执行其他操作,直接就是调用了takescreenshot方法,这样我们继续看一下takescreenshot方法的实现。

private void takescreenshot() {
    synchronized (mscreenshotlock) {
      if (mscreenshotconnection != null) {
        return;
      }
      componentname cn = new componentname("com.android.systemui",
          "com.android.systemui.screenshot.takescreenshotservice");
      intent intent = new intent();
      intent.setcomponent(cn);
      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, 1);
            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) {}
      };
      if (mcontext.bindserviceasuser(
          intent, conn, context.bind_auto_create, userhandle.current)) {
        mscreenshotconnection = conn;
        mhandler.postdelayed(mscreenshottimeout, 10000);
      }
    }
  }

可以发现这里通过反射机制创建了一个takescreenshotservice对象然后调用了bindserviceasuser,这样就创建了takescreenshotservice服务并在服务创建之后发送了一个异步消息。好了,我们看一下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) {
      switch (msg.what) {
        case 1:
          final messenger callback = msg.replyto;
          if (mscreenshot == null) {
            mscreenshot = new globalscreenshot(takescreenshotservice.this);
          }
          mscreenshot.takescreenshot(new runnable() {
            @override public void run() {
              message reply = message.obtain(null, 1);
              try {
                callback.send(reply);
              } catch (remoteexception e) {
              }
            }
          }, msg.arg1 > 0, msg.arg2 > 0);
      }
    }
  };

  @override
  public ibinder onbind(intent intent) {
    return new messenger(mhandler).getbinder();
  }
}

可以发现在在takescreenshotservice类的定义中有一个handler成员变量,而我们在启动takescreentshowservice的时候回发送一个异步消息,这样就会执行mhandler的handlemessage方法,然后在handlemessage方法中我们创建了一个globalscreenshow对象,然后执行了takescreenshot方法,好吧,继续看一下takescreentshot方法的执行逻辑。

/**
   * takes a screenshot of the current display and shows an animation.
   */
  void takescreenshot(runnable finisher, boolean statusbarvisible, boolean navbarvisible) {
    // 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);
      finisher.run();
      return;
    }

    if (requiresrotation) {
      // rotate the screenshot to the current orientation
      bitmap ss = bitmap.createbitmap(mdisplaymetrics.widthpixels,
          mdisplaymetrics.heightpixels, bitmap.config.argb_8888);
      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;
    }

    // optimizations
    mscreenbitmap.sethasalpha(false);
    mscreenbitmap.preparetodraw();

    // start the post-screenshot animation
    startanimation(finisher, mdisplaymetrics.widthpixels, mdisplaymetrics.heightpixels,
        statusbarvisible, navbarvisible);
  }

可以看到这里后两个参数:statusbarvisible,navbarvisible是否可见,而这两个参数在我们

phonewindowmanager.takescreenshot方法传递的:

if (mstatusbar != null && mstatusbar.isvisiblelw())
              msg.arg1 = 1;
            if (mnavigationbar != null && mnavigationbar.isvisiblelw())
              msg.arg2 = 1;

可见若果mstatusbar可见,则传递的statusbarvisible为true,若mnavigationbar可见,则传递的navbarvisible为true。然后我们在截屏的时候判断nstatusbar是否可见,mnavigationbar是否可见,若可见的时候则截屏同样将其截屏出来。继续回到我们的takescreenshot方法,然后调用了:

// take the screenshot
mscreenbitmap = surfacecontrol.screenshot((int) dims[0], (int) dims[1]);

方法,看注释,这里就是执行截屏事件的具体操作了,然后我看一下surfacecontrol.screenshot方法的具体实现,另外这里需要注意的是,截屏之后返回的是一个bitmap对象,其实熟悉android绘制机制的童鞋应该知道android中所有显示能够显示的东西,在内存中表现都是bitmap对象。

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之后,判断是否截屏成功:

if (mscreenbitmap == null) {
      notifyscreenshoterror(mcontext, mnotificationmanager);
      finisher.run();
      return;
    }

若截屏之后,截屏的bitmap对象为空,这里判断截屏失败,调用了notifyscreenshoterror方法,发送截屏失败的notification通知。

static void notifyscreenshoterror(context context, notificationmanager nmanager) {
    resources r = context.getresources();

    // clear all existing notification, compose the new notification and show it
    notification.builder b = new notification.builder(context)
      .setticker(r.getstring(r.string.screenshot_failed_title))
      .setcontenttitle(r.getstring(r.string.screenshot_failed_title))
      .setcontenttext(r.getstring(r.string.screenshot_failed_text))
      .setsmallicon(r.drawable.stat_notify_image_error)
      .setwhen(system.currenttimemillis())
      .setvisibility(notification.visibility_public) // ok to show outside lockscreen
      .setcategory(notification.category_error)
      .setautocancel(true)
      .setcolor(context.getcolor(
            com.android.internal.r.color.system_notification_accent_color));
    notification n =
      new notification.bigtextstyle(b)
        .bigtext(r.getstring(r.string.screenshot_failed_text))
        .build();
    nmanager.notify(r.id.notification_screenshot, n);
  }

然后继续看takescreenshot方法,判断截屏的图像是否需要旋转,若需要的话,则旋转图像:

if (requiresrotation) {
      // rotate the screenshot to the current orientation
      bitmap ss = bitmap.createbitmap(mdisplaymetrics.widthpixels,
          mdisplaymetrics.heightpixels, bitmap.config.argb_8888);
      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;
    }

在takescreenshot方法的最后若截屏成功,我们调用了:

// start the post-screenshot animation
    startanimation(finisher, mdisplaymetrics.widthpixels, mdisplaymetrics.heightpixels,
        statusbarvisible, navbarvisible);

开始截屏的动画,好吧,看一下动画效果的实现:

/**
   * starts the animation after taking the screenshot
   */
  private void startanimation(final runnable finisher, int w, int h, boolean statusbarvisible,
      boolean navbarvisible) {
    // add the view for the animation
    mscreenshotview.setimagebitmap(mscreenbitmap);
    mscreenshotlayout.requestfocus();

    // setup the animation with the screenshot just taken
    if (mscreenshotanimation != null) {
      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();
      }
    });
  }

好吧,经过着一些列的操作之后我们实现了截屏之后的动画效果了,这里暂时不分析动画效果,我们看一下动画效果之后做了哪些?还记不记的一般情况下我们截屏之后都会收到一个截屏的notification通知?这里应该也是在其animatorlisteneradapter的onanimationend方法中实现的,也就是动画执行完成之后,我们看一下其savescreenshotinworkerthread方法的实现:

/**
   * creates a new worker thread and saves the screenshot to the media store.
   */
  private void savescreenshotinworkerthread(runnable finisher) {
    saveimageinbackgrounddata data = new saveimageinbackgrounddata();
    data.context = mcontext;
    data.image = mscreenbitmap;
    data.iconsize = mnotificationiconsize;
    data.finisher = finisher;
    data.previewwidth = mpreviewwidth;
    data.previewheight = mpreviewheight;
    if (msaveinbgtask != null) {
      msaveinbgtask.cancel(false);
    }
    msaveinbgtask = new saveimageinbackgroundtask(mcontext, data, mnotificationmanager,
        r.id.notification_screenshot).execute(data);
  }

好吧,这里主要逻辑就是构造了一个saveimageinbackgroundtask对象,看样子发送截屏成功的通知应该是在这里实现的,我们看一下saveimageinbackgroundtask构造方法的实现逻辑:

saveimageinbackgroundtask(context context, saveimageinbackgrounddata data,
      notificationmanager nmanager, int nid) {
    ...

    // show the intermediate notification
    mtickeraddspace = !mtickeraddspace;
    mnotificationid = nid;
    mnotificationmanager = nmanager;
    final long now = system.currenttimemillis();

    mnotificationbuilder = new notification.builder(context)
      .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)
      .setcolor(r.getcolor(com.android.internal.r.color.system_notification_accent_color));

    mnotificationstyle = new notification.bigpicturestyle()
      .bigpicture(picture.createashmembitmap());
    mnotificationbuilder.setstyle(mnotificationstyle);

    // for "public" situations we want to show all the same info but
    // omit the actual screenshot image.
    mpublicnotificationbuilder = new notification.builder(context)
        .setcontenttitle(r.getstring(r.string.screenshot_saving_title))
        .setcontenttext(r.getstring(r.string.screenshot_saving_text))
        .setsmallicon(r.drawable.stat_notify_image)
        .setcategory(notification.category_progress)
        .setwhen(now)
        .setcolor(r.getcolor(
            com.android.internal.r.color.system_notification_accent_color));

    mnotificationbuilder.setpublicversion(mpublicnotificationbuilder.build());

    notification n = mnotificationbuilder.build();
    n.flags |= notification.flag_no_clear;
    mnotificationmanager.notify(nid, n);

    // on the tablet, the large icon makes the notification appear as if it is clickable (and
    // on small devices, the large icon is not shown) so defer showing the large icon until
    // we compose the final post-save notification below.
    mnotificationbuilder.setlargeicon(icon.createashmembitmap());
    // but we still don't set it for the expanded view, allowing the smallicon to show here.
    mnotificationstyle.biglargeicon((bitmap) null);
  }

可以发现在构造方法的后面狗仔了一个notificationbuilder对象,然后发送了一个截屏成功的notification,这样我们在截屏动画之后就收到了notification的通知了。

总结:

一般默认情况下按下音量减少键和开机键会执行截图动作,程序执行的入口就在在phonewindowmanager的dispatchunhandledkey方法中;然后通过takescreenshotservice服务执行截图逻辑;通过nativie方法获取截图的bitmap,如果失败调用失败通知栏消息,如果成功调用截图动画后发送成功通知栏消息。

如有疑问请留言或者到本站社区交流讨论,感谢阅读,希望能帮助到大家,谢谢大家对本站的支持! 

上一篇:

下一篇: