Android VirtualDisplay的用法和遇到的点击事件问题

前言

     最近在看Android 多屏相关的问题,初步接触到了DisplayManager的一些东西。Android支持多个屏幕显示,手机的屏幕为主屏,通过HDMI,usb,wifi连接的屏幕为外显,还有一种是Android系统提供的Api即VirtualDisplay。

 一,MediaRouter 或 DisplayManager

     Android获取辅助屏幕(即外显)的方式有两种, MediaRouter 或 DisplayManager。Android 4.2的手机在开发中选项中,都有模拟辅助屏幕的功能,我们选择一个分辨率,打开它,模拟一个外部的屏幕。这个屏幕是开发者选项搞出来的,所以并不真实存在,模拟出来的(为了调试而用), 如果手机连接到一个辅助屏幕时,那个真实的屏幕才是手机的辅助屏幕。

1,MediaRouter

MediaRouter 用于和 MediaRouterService交互一起管理多媒体的播放行为,并维护当前已经配对上的remote display设备,包括WiFi diplay、蓝牙A2DP设备、Google Cast设备

Android媒体路由框架提供两种播放输出类型:远端播放和辅助输出。

远端播放类型 指的是辅助设备处理媒体内容的接收、解码和回放,而Android设备只起远程控制作用,如Google Cast。

辅助输出类型 则是应用本身处理媒体内容,包括媒体内容的引出和处理,并把处理结果直接呈现和串流到辅助接收设备上,辅助接收设备只是呈现媒体处理后的最终内容,如Android系统中使用该方式用来支持Wireless Display输出。这不是投屏吗?

媒体应用通过一个MediaRouter对象(媒体路由框架提供的一个对象)来使用媒体路由框架,用来选择媒体路由,并经过媒体路由框架的路由连接到选择的最终接收设备。

获得一个MediaRouter对象的方式为

MediaRouter mediaRouter = (MediaRouter) getSystemService(Context.MEDIA_ROUTER_SERVICE);

MediaRouter中常用的方法有以下:

RouterInfo是远端设备的信息类。

//获得RouterInfo的一个实例

MediaRouter.RouteInfo localRouteInfo = 
mediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
display = localRouteInfo != null ? localRouteInfo.getPresentationDisplay() : null;
if (display != null) {
    showPresentation(display);
}
Presentation
presentation 是一种特殊的 dialog ,目的是为了在辅助屏幕上展示不同的内
presentation 是一个 dialog
根据生物遗传学的角度,presentation 无论被描述成什么天花乱坠的模样,它也是一个dialog。presentation 目的是显示在辅助屏幕上。

2,DisplayManager

DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);

(1)获得当前存在的Displays

Display[] arrayOfDisplay = displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);//仅获取副屏

if (arrayOfDisplay.length > 0) { showPresentation(arrayOfDisplay[0]);//取第一个分屏使用 } 或者Display[] displays = displayManager.getDisplays();//获取系统所有的Display,包括桌面本身所在的Display

if (displays.length > 1) {

 presentation2 = new MyPresentation2(getApplicationContext(), presentationDisplays[1]);       presentation2.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); presentation2.show();

}

(2)创建一个虚屏VirtualDisplay,内含一个真实的Display对象

int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; //创建公共Display displayManager.createVirtualDisplay("displayName", width, height, densityDpi, mSurface, flags);

createVirtualDisplay()方法中有宽、高、屏幕密度
densityDpi:屏幕密度 ,一般选320,480,960等,值不同效果不同,使用的时候再去细看。我在项目中选用的480。

mSurface:创建VirtualDisplay要依赖的Surface,一种是从SurfaceView中获得, 一种是从ImageReader中获取。SurfaceView是view的子类,没有实现ViewGroup类,不能在SurfaceView上实现添加其他控件。SurfaceView提供直接访问一个可画图的界面,可以控制在界面顶部的子视图层。SurfaceView是提供给需要直接画像素而不是使用
窗体部件的应用使用的。Android图形系统中一个重要的概念和线索是surface。View及其子类(如TextView, Button)要画在surface上。每个surface创建一个Canvas对象(但属性时常改变),用来管理view在surface上的绘图操作,如画点画线。

flags:是创建虚屏的一些标志位。    

(3)VirtualDisplay展示内容

  (a)展示一个Presentation

      Presentation前面已说过, 本质是一个Dialog, 在VirtualDisplay中以Presentation为展示View的做法是自定义一个Presentation子类MainPresentation,setContentView()xml文件。然后创建一个Presentation实例

Presentation presentation = new TestPresentation(this, mDisplay.getDisplay());              
presentation.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
presentation.show();

(b)展示一个Activity(如何将一个Activity启动到一个副屏上)

 ActivityOptions options = null;
 if (mDisplay != null) {
     options = ActivityOptions.makeBasic();
     options.setLaunchDisplayId(mDisplay.getDisplay().getDisplayId());
  }
  Intent i = new Intent();
  i.setClassName(getPackageName(), getPackageName()+ ".SecondActivity");
  i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
  if (options != null) {
      startActivity(i, options.toBundle());
  }

值得注意的是,在副屏上展示一个Activity时,需要用显式启动的方式,启动的目标Activity在AndroidManifest.xml中声明属性android:exported = true.

二、遇到的点击事件的问题

    我在Activity中定义了一个SurfaceView,创建好之后, 以SurfaceView的Surface为参,创建了一个VirtualDisplay,然后在VirtualDisplay上又展示了一个Presentation(内设了layout布局文件)。

public class VirtualDisplayActivity extends AppCompatActivity {
    private SurfaceView mSurfaceView;
    private DisplayManager mDisplayManager;
    private SurfaceHolder mSurfaceHolder;
    private mWidth;
    private mHeight;
    private SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback(){
        @Override
        public void surfaceCreated(@NonNull SurfaceHolder holder) {
            mSurface = mSurfaceHolder.getSurface();
        }

        @Override
        public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
            mWidth = width;
            mHeight = height;
            createVirtualAndShowPresentation();
        }

        @Override
        public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
          
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.act_vir);
        mSurfaceView = findViewById(R.id.surface);
        mSurfaceHolder = mSurfaceView.getHolder();
        mDisplayManager = (DisplayManager) getSystemService(DISPLAY_SERVICE);
        getSupportActionBar().setTitle("啦啦啦啦啦");
    }
    private void createVirtualAndShowPresentation() {
        int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
        mDisplay = mDisplayManager.createVirtualDisplay("maomao",mWidth, mHeight, 480, mSurface,flags);
        Presentation presentation = new TestPresentation(this, mDisplay.getDisplay());
        presentation.getWindow().
setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
        presentation.show();
        
    }
}
public class TestPresentation extends Presentation {
    Button button;
    int conut = 0;
    public TestPresentation(Context outerContext, Display display) {
        super(outerContext, display);
        setContentView(R.layout.layout_presentation);
        button = findViewById(R.id.clickBtn);
        button.setOnClickListener(v -> {
            button.setText(String.valueOf(conut));
            conut++;
            Log.e("maomao","我收到event了 click Presentation = " + conut);
        });
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e("maomao"," dispatchTouchEvent 我收到event了吗");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("maomao"," onTouchEvent 我收到event了吗");
        return super.onTouchEvent(event);
    }
}

最后运行效果为

那么问题来了。Presentation里的控件点击事件不响应。甚至VirtualDisplay整个界面都不响应touch事件,最外层能响应touch事件的是SurfaceView。重写SurfaceView的onTouchEvent(),能收到触摸事件,但Presentation死活都收不到触摸事件。

我做了以下实验,

 (1)如果给SurfaceView设置背景色红色,那么Presentation的布局就不显示了。说明SurfaceView还是作为View在View层级里,显示在Presentation之上的(要知道Presentation是画在Surface上, 单独存在SurfacFlinger中)。

(2) 将VirtualDisplayActivity所在的window设成点击事件可穿透的,我的做法是

 WindowManager.LayoutParams mLayoutParams = getWindow().getAttributes();
 mLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
 mLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 getWindow().setAttributes(mLayoutParams);

然后发现,SurfaceView收不到触摸事件,此时Presentation控件点击还是没反应。 

(3) 我把Presentation显示在Activity所在的getWindow()所在的getDisplay(),发现能点击,完全没毛病。

    于是我做了我的推理: SurfaceView是一个view子类。它接收到的事件到自身就不再分发了, 至于VirtualDisplay以及展示的Presentation都是画在它内部的Surface上的,怎么能接收到touch事件呢??所以SurfaceView里的Surface展示的VirtualDisplay内的页面是无法点击的!! 不知道我分析的对不对。看到的同学指教一下啊啊啊~

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------6 6月10号更新, 继续讨论SurfaceView + VirtualDisplay + Presentation/Activity 可点击问题

一、 结论: 无法穿透SurfaceView进行点击

         以SurfaceView的Surface 为参创建VirtualDisplay, 这样VirtualDisplay里不管展示Presentation还是Activity 都无法点击, 因为Presentation还是Activity都是画在Surface里面的, 点击事件到达SurfaceView就已经是View的子类了,即到了View层级的最顶层。

二、 解决方法:

        实现一个SurfaceView的子类,重写SurfaceView的onTouchEvent()方法,在SurfaceView接收到点击事件之后,原封不动的将event 手动分发到VirtualDisplay的Presentation和Activity内。

       

public class MainSurfaceView extends SurfaceView {
    TouchEventDispatcher mDispatcher;
    public MainSurfaceView(Context context) {
        super(context);
    }
    public MainSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MainSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    public void setTouchEventDispatcher(TouchEventDispatcher dispatcher) {
        mDispatcher = dispatcher;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mDispatcher != null) {
            mDispatcher.dispatchTouchEvent(event);
        }
        return super.onTouchEvent(event);
    }
    public interface TouchEventDispatcher {
        void dispatchTouchEvent(MotionEvent event);
    }
}

  给MainSurfaceView设置TouchEventDispatcher 代码为

mSurfaceView.setTouchEventDispatcher(event -> {
            if (mPresentation != null) {
                mPresentation.dispatchTouchEventAgain(event);
            }
});

TestPresentation里的dispatchTouchEventAgain()方法为

public void dispatchTouchEventAgain(MotionEvent event) {
        getWindow().getDecorView().dispatchTouchEvent(event);
}

至此,SurfaceView的点击事件就被我们手动的传递到了VirtualDisplay内部的View层级中。 

                      

 

 

 

 

 

 

 

 

 

 

 

  • 15
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
### 回答1: Android VirtualDisplay是一个Android平台上的API,它允许应用程序创建虚拟显示器并将其用作外部显示器。这个API可以用于实现屏幕共享、远程桌面、投影等功能。通过VirtualDisplay,应用程序可以将自己的内容显示在虚拟显示器上,然后将其传输到外部设备上,从而实现屏幕共享或投影。同时,VirtualDisplay还可以用于创建虚拟显示器,以便在不需要物理显示器的情况下测试应用程序的UI。 ### 回答2: Android VirtualDisplayAndroid 操作系统中的一种功能,它允许应用程序在未连接到物理显示器的情况下创建虚拟显示器。它的应用场景主要是基于屏幕投影和屏幕录制。 首先, VirtualDisplay 可以实现屏幕投影功能,也就是将 Android 设备的屏幕内容投影到其它设备屏幕上,比如将手机屏幕投影到电视屏幕上。这个功能常用于演示或教育讲解等场合,用户可以通过一个代码库快速地实现屏幕投影功能。 其次, VirtualDisplay 可以实现屏幕录制功能。通过虚拟化一个屏幕显示器,将屏幕上的内容编码为视频流,然后可以把这个视频流储存在文件中或者通过网络传输到其它设备上。用户可以通过这个功能来录制自己的屏幕操作、制作教程视频等。 在使用 VirtualDisplay 的过程中,需要借助一些 Android 内部类,比如 DisplayManager、MediaProjection、ImageReader 等类来实现虚拟显示器的创建和管理。同时,由于 VirtualDisplay 创建的屏幕是虚拟的,不受任何物理显示器的限制,所以它可以自由地调整屏幕分辨率、刷新率等参数,以达到最佳的屏幕投影或录制效果。 总之, Android VirtualDisplay 是一个非常实用的功能,它可以让应用程序开发者在不依赖物理显示器的情况下实现屏幕投影和录制等功能,为用户带来普遍的便利。 ### 回答3: Android VirtualDisplay指的是Android系统中的一个虚拟显示器。它可以让开发人员创建一个模拟器,模拟不同分辨率、不同DPI的设备屏幕,方便开发人员测试和调试自己的应用。同时,VirtualDisplay还可以将内容展示到一个外部设备,例如连接到电视、显示器或者投影仪上,实现屏幕扩展或者镜像功能。 VirtualDisplayAndroid系统中属于硬件抽象层(HAL)模块,可以通过Java或者NDK API进行访问和控制。VirtualDisplay在应用领域有着广泛的应用,比如屏幕共享、多窗口管理等。在Android 5.0 Lollipop系统中,多窗口模式就是通过VirtualDisplay实现的。 使用VirtualDisplay的步骤比较简单,首先需要获取一个DisplayManager实例,然后使用createVirtualDisplay()方法创建一个VirtualDisplay对象。在创建VirtualDisplay时可以指定屏幕大小、像素密度、显示标识符等参数。创建好VirtualDisplay之后,就可以将内容展示到该VirtualDisplay上,实现屏幕扩展或者镜像功能。 总之,Android VirtualDisplayAndroid系统中的一个重要组件,可以方便地模拟不同设备屏幕,在应用开发和调试中起到了很大的作用。同时,VirtualDisplay还可以实现屏幕扩展和镜像功能,在多屏工作环境中也有着广泛的应用。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值