热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

Android实现通话最小化悬浮框效果

本片内容给大家介绍了Android音视频通话过程中最小化成悬浮框的实现的方法以及代码写法。

大家在使用主流的视频软件以及直播软件的时候,经常会看到打开视频最小化以后,不是直接关闭,而是在屏幕右下角一个小窗口的样子,本次小编就给大家带来的是用Android实现在视频或者语音通话的时候,最小化也是出现一个悬浮框的效果。

关于音视频通话过程中最小化成悬浮框这个功能的实现,网络上类似的文章很多,但是好像还没看到解释的较为清晰的,这里因为项目需要实现了这样的一个功能,今天我把它记录下来,一方面为了以后用到便于自己查阅,一方面也给有需要的人提供一个思路,让大家少走弯路。这里我也是参考了些有关Android悬浮框的文章,再结合自己的理解所实现出来的,可能实现的方法不是最好,但是这或许也是一个可行的方案。

一、实现效果(gif效果可能录制的不是特别好)

二、实现思路

关于这个功能的实现其实不难,这里我把实现思路拆分为了两步:1、视频通话Activity的最小化。 2、视频通话悬浮框的开启

具体思路是这样的:当用户点击最小化按钮的时候,最小化我们的视频通话Activity(这时Activity处于后台状态),移除原先在Activity的视频画布(因为我用的是网易云信,这里他们只能允许一个视频画布存在,这里看情况要不要移除),于此同时,延时个几百毫秒,开启悬浮框,新建一个新的视频画布然后动态添加到悬浮框里面去,监听悬浮框的触摸事件,让悬浮框可以拖拽移动;监听悬浮框的点击事件,如果用户点击了悬浮框,则移除悬浮框里面新建的那个视频画布,然后重新调起我们在后台的视频通话Activity,紧接着新建一个新的视频画布重新动态的添加到Activity里面去。关于视频画布的添加移除方法,这里要看一下所接入的第三方SDK,如用的若是网易云信的SDK,他们的方法如下(下面摘自他们的SDK说明文档),也就是说移除画布我只需要传入null就行了。

1.Activity是如何实现最小化的?

Activity最小化可能你没有听过,但是只要姿势对的话,其实实现起来非常简单,因为Activity本身就自带了一个moveTaskToBack(boolean nonRoot),如果我们要实现最小化,只需要调用moveTaskToBack(true)传入一个true值就可以了,但是这里有一个前提,就是需要设置Activity的启动模式为singleInstance模式,两步搞定。(注:这里先记住一个小知识点,就是activity最小化后重新从后台回到前台会回调onRestart()方法)

@Override
public boolean moveTaskToBack(boolean nonRoot) {
return super.moveTaskToBack(nonRoot);
}

2.悬浮框是如何开启的?

这里我把悬浮框的实现方法写在一个服务Service里面,将悬浮框的开启关闭与服务Service的绑定解绑所关联起来,开启服务即相当于开启我们的悬浮框,解绑服务则相当于关闭关闭的悬浮框,以此来达到更好的控制效果。

a. 首先我们声明一个服务类,取名为FloatVideoWindowService:

public class FloatVideoWindowService extends Service {

 @Nullable
 @Override
 public IBinder onBind(Intent intent) {
  return new MyBinder();
 }

 public class MyBinder extends Binder {
  public FloatVideoWindowService getService() {
   return FloatVideoWindowService.this;
  }
 }

 @Override
 public void onCreate() {
  super.onCreate();
 }

 @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
  return super.onStartCommand(intent, flags, startId);
 }

 @Override
 public void onDestroy() {
  super.onDestroy();
 }
}

b. 为悬浮框建立一个布局文件alert_float_video_layout,这里根据需求去写,如果只是像我上面gif那样,只需要悬浮框显示对方的视频画布,那么布局文件可以如下所示:(其中悬浮框大小我这里固定为长80dp,高110dp,id为small_size_preview的Linearlayout主要是一个容器,可以动态的添加view到里面去,也就是我们的视频画布)

<&#63;xml version="1.0" encoding="utf-8"&#63;>


 

c. 布局定义好后,接下来就要对悬浮框做一些初始化操作了,初始化操作这里我们放在服务的onCreate()生命周期里面执行,因为只需要执行一次就行了。这里的初始化主要包括对:悬浮框的基本参数(位置,宽高等),悬浮框的点击事件以及悬浮框的触摸事件(即可拖动范围)等的设置,代码注释已经很清楚,直接看代码,如下所示:

public class FloatVideoWindowService extends Service {
 private WindowManager mWindowManager;
 private WindowManager.LayoutParams wmParams;
 private LayoutInflater inflater;

 //constant
 private boolean clickflag;

 //view
 private View mFloatingLayout; //浮动布局
 private LinearLayout smallSizePreviewLayout; //容器父布局

 @Nullable
 @Override
 public IBinder onBind(Intent intent) {
  return new MyBinder();
 }

 public class MyBinder extends Binder {
  public FloatVideoWindowService getService() {
   return FloatVideoWindowService.this;
  }
 }

 @Override
 public void onCreate() {
  super.onCreate();
  initWindow();//设置悬浮窗基本参数(位置、宽高等)
  initFloating();//悬浮框点击事件的处理
 }

 @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
  return super.onStartCommand(intent, flags, startId);
 }

 
 @Override
 public void onDestroy() {
  super.onDestroy();
 }

 /**
  * 设置悬浮框基本参数(位置、宽高等)
  */
 private void initWindow() {
  mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
  wmParams = getParams();//设置好悬浮窗的参数
  // 悬浮窗默认显示以左上角为起始坐标
  wmParams.gravity = Gravity.LEFT | Gravity.TOP;
  //悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0
  wmParams.x = 70;
  wmParams.y = 210;
  //得到容器,通过这个inflater来获得悬浮窗控件
  inflater = LayoutInflater.from(getApplicationContext());
  // 获取浮动窗口视图所在布局
  mFloatingLayout = inflater.inflate(R.layout.alert_float_video_layout, null);
  // 添加悬浮窗的视图
  mWindowManager.addView(mFloatingLayout, wmParams);
 }

 
 private WindowManager.LayoutParams getParams() {
  wmParams = new WindowManager.LayoutParams();
  //设置window type 下面变量2002是在屏幕区域显示,2003则可以显示在状态栏之上
  wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;
  //设置可以显示在状态栏上
  wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
    WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR |
    WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;

  //设置悬浮窗口长宽数据
  wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
  wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
  return wmParams;
 }

  
 private void initFloating() {
  smallSizePreviewLayout = mFloatingLayout.findViewById(R.id.small_size_preview);

  //悬浮框点击事件
  smallSizePreviewLayout.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
     //在这里实现点击重新回到Activity
   }
  });

  //悬浮框触摸事件,设置悬浮框可拖动
  smallSizePreviewLayout.setOnTouchListener(new FloatingListener());
 }

 //开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标)
 private int mTouchStartX, mTouchStartY, mTouchCurrentX, mTouchCurrentY;
 //开始时的坐标和结束时的坐标(相对于自身控件的坐标)
 private int mStartX, mStartY, mStopX, mStopY;
   //判断悬浮窗口是否移动,这里做个标记,防止移动后松手触发了点击事件
 private boolean isMove;

 private class FloatingListener implements View.OnTouchListener {

  @Override
  public boolean onTouch(View v, MotionEvent event) {
   int action = event.getAction();
   switch (action) {
    case MotionEvent.ACTION_DOWN:
     isMove = false;
     mTouchStartX = (int) event.getRawX();
     mTouchStartY = (int) event.getRawY();
     mStartX = (int) event.getX();
     mStartY = (int) event.getY();
     break;
    case MotionEvent.ACTION_MOVE:
     mTouchCurrentX = (int) event.getRawX();
     mTouchCurrentY = (int) event.getRawY();
     wmParams.x += mTouchCurrentX - mTouchStartX;
     wmParams.y += mTouchCurrentY - mTouchStartY;
     mWindowManager.updateViewLayout(mFloatingLayout, wmParams);

     mTouchStartX = mTouchCurrentX;
     mTouchStartY = mTouchCurrentY;
     break;
    case MotionEvent.ACTION_UP:
     mStopX = (int) event.getX();
     mStopY = (int) event.getY();
     if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {
      isMove = true;
     }
     break;
   }

   //如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件
   return isMove;
  }
 }
}

d. 在悬浮框成功被初始化以及相关参数被设置后,接下来就需要将对方的视频画布添加到悬浮框里面去了,这样我们才能看到对方的视频画面嘛,同样我们是在Service的oncreate这个生命周期完成这个操作的,这里视频画布的添加方式使用的网易云信的SDK,具体的添加方式视不同的SDK而定,代码如下所示:

/**
  * 初始化预览窗口
  */
 private void initSurface() {
  if (smallRender == null) {
   smallRender = new AVChatSurfaceViewRenderer(getApplicationContext());
  }

  addIntoSmallSizePreviewLayout(smallRender);
 }

 /**
  * 添加surfaceview到smallSizePreviewLayout
  */
 private void addIntoSmallSizePreviewLayout(SurfaceView surfaceView) {
  if (surfaceView.getParent() != null) {
   ((ViewGroup) surfaceView.getParent()).removeView(surfaceView);
  }

  smallSizePreviewLayout.addView(surfaceView);
  surfaceView.setZOrderMediaOverlay(true);
 }

e. 我们上面说到要将服务service的绑定与解绑与悬浮框的开启和关闭相结合,所以既然我们在服务的oncreate()方法中开启了悬浮框,那么就应该在其ondestroy()方法中对悬浮框进行关闭,关闭悬浮框的本质是将相关view给移除掉,接着清除我们的视频画布,在服务的ondestroy()方法中执行如下代码:

@Override
 public void onDestroy() {
  super.onDestroy();
  if (mFloatingLayout != null) {
   // 移除悬浮窗口
   mWindowManager.removeView(mFloatingLayout);
  }

  //清除视频画布
  AVChatManager.getInstance().setupRemoteVideoRender(account, null, false, 0);
 }

f. 服务的绑定方式有bindService和startService两种,使用不同的绑定方式其生命周期也会不一样,已知我们需要让悬浮框在视频通话activity finish掉的时候也顺便关掉,那么理所当然我们就应该采用bind方式来启动服务,让他的生命周期跟随他的开启者,也即是跟随开启它的activity生命周期。

intent = new Intent(this, FloatVideoWindowService.class);//开启服务显示悬浮框
bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE);

ServiceConnection mVideoServiceCOnnection= new ServiceConnection() {

  @Override
  public void onServiceConnected(ComponentName name, IBinder service) {
   // 获取服务的操作对象
   FloatVideoWindowService.MyBinder binder = (FloatVideoWindowService.MyBinder) service;
   binder.getService();
  }

  @Override
  public void onServiceDisconnected(ComponentName name) {
  }
 };

三、完整的流程

现在我们将上面所说的给串联起来,思路会更加清晰一点,假设现在我正在进行视频通话,点击视频最小化按钮,我们应该按顺序执行如下步骤:(如果你姿势对的话,现在应该是会出现个悬浮框了)

public void startVideoService() {
   moveTaskToBack(true);//最小化Activity
   intent = new Intent(this, FloatVideoWindowService.class);//开启服务显示悬浮框
   bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE);
 }

当我们点击悬浮框的时候,可以使用startActivity(intent)来再次打开我们的activity,这时候视频通话activity会回调onRestart()方法,我们在onRestart()生命周期里面unbind解绑掉悬浮框服务,并且重新设置新的视频画布到activity上

@Override
 protected void onRestart() {
  super.onRestart();
  unbindService(mVideoServiceConnection);//不显示悬浮框

  //从悬浮窗进来后重新设置画布(判断是不是接通了)
  if (isCallEstablished) {
   //如果接通,先清除所有画布
   avChatUI.clearAllSurfaceView(avChatUI.getAccount());
   //延迟重新加载远端和本地的视频画布
   mHandler.postDelayed(new Runnable() {
    @Override
    public void run() {
     avChatUI.initAllSurfaceView(avChatUI.getAccount());
     
    }
   }, 800);
  } else {
   //如果没接通,直接初始化所有画布
   avChatUI.initLargeSurfaceView(IMCache.getAccount());
  }
 }

以上就是本次为大家分享的关于Android开发的又一功能实现方式,希望我们整理的能够帮助到你。


推荐阅读
  • QUIC协议:快速UDP互联网连接
    QUIC(Quick UDP Internet Connections)是谷歌开发的一种旨在提高网络性能和安全性的传输层协议。它基于UDP,并结合了TLS级别的安全性,提供了更高效、更可靠的互联网通信方式。 ... [详细]
  • 深入理解OAuth认证机制
    本文介绍了OAuth认证协议的核心概念及其工作原理。OAuth是一种开放标准,旨在为第三方应用提供安全的用户资源访问授权,同时确保用户的账户信息(如用户名和密码)不会暴露给第三方。 ... [详细]
  • 2023 ARM嵌入式系统全国技术巡讲旨在分享ARM公司在半导体知识产权(IP)领域的最新进展。作为全球领先的IP提供商,ARM在嵌入式处理器市场占据主导地位,其产品广泛应用于90%以上的嵌入式设备中。此次巡讲将邀请来自ARM、飞思卡尔以及华清远见教育集团的行业专家,共同探讨当前嵌入式系统的前沿技术和应用。 ... [详细]
  • 国内BI工具迎战国际巨头Tableau,稳步崛起
    尽管商业智能(BI)工具在中国的普及程度尚不及国际市场,但近年来,随着本土企业的持续创新和市场推广,国内主流BI工具正逐渐崭露头角。面对国际品牌如Tableau的强大竞争,国内BI工具通过不断优化产品和技术,赢得了越来越多用户的认可。 ... [详细]
  • 本文详细分析了JSP(JavaServer Pages)技术的主要优点和缺点,帮助开发者更好地理解其适用场景及潜在挑战。JSP作为一种服务器端技术,广泛应用于Web开发中。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 本文详细介绍了如何使用Spring Boot进行高效开发,涵盖了配置、实例化容器以及核心注解的使用方法。 ... [详细]
  • 本章将深入探讨移动 UI 设计的核心原则,帮助开发者构建简洁、高效且用户友好的界面。通过学习设计规则和用户体验优化技巧,您将能够创建出既美观又实用的移动应用。 ... [详细]
  • 本文介绍如何在应用程序中使用文本输入框创建密码输入框,并通过设置掩码来隐藏用户输入的内容。我们将详细解释代码实现,并提供专业的补充说明。 ... [详细]
  • 本文介绍如何通过SQL查询从JDE(JD Edwards)系统中提取所有字典数据,涵盖关键表的关联和字段选择。具体包括F0004和F0005系列表的数据提取方法。 ... [详细]
  • 本文详细介绍了如何通过命令行启动MySQL服务,包括打开命令提示符窗口、进入MySQL的bin目录、输入正确的连接命令以及注意事项。文中还提供了更多相关命令的资源链接。 ... [详细]
  • 本文介绍如何使用 NSTimer 实现倒计时功能,详细讲解了初始化方法、参数配置以及具体实现步骤。通过示例代码展示如何创建和管理定时器,确保在指定时间间隔内执行特定任务。 ... [详细]
  • 2023年京东Android面试真题解析与经验分享
    本文由一位拥有6年Android开发经验的工程师撰写,详细解析了京东面试中常见的技术问题。涵盖引用传递、Handler机制、ListView优化、多线程控制及ANR处理等核心知识点。 ... [详细]
  • 本文介绍如何在 Unity 的 XML 配置文件中,将参数传递给自定义生命周期管理器的构造函数。我们将详细探讨 CustomLifetimeManager 类的实现及其配置方法。 ... [详细]
  • 基于KVM的SRIOV直通配置及性能测试
    SRIOV介绍、VF直通配置,以及包转发率性能测试小慢哥的原创文章,欢迎转载目录?1.SRIOV介绍?2.环境说明?3.开启SRIOV?4.生成VF?5.VF ... [详细]
author-avatar
包子F3R
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有