Java:复制一个目录下的所有文件到另外一个目录

     首先说说我的思路, 要复制一个目录下的所有文件到另外的一个目录下,我们不知道目录下的结构是怎么样的,也不知道目录有多少层,文件有多少个,这样我们会想用循环,for! 但是我们不知道有多少层,所以循环不能够满足我们的需求! 学过递归的人,都知道这个用递归的思想可以很好解决这个问题的。

       递归这里我就不说是什么东西了,这个自己可以百度,谷歌!

      

     现在说说我的实现, 因为目录下有可能存在目录或者文件,他们混在一起,所以我们很难进行操作和判断,如果直接操作的话,我们要先建立目录,才能定位文件。这个操作起来比较复制。 所以我想到的是,先复制目录的结构,再复制文件这个思路!下面是我的代码实现, 

       1.先写一个文件复制的操作方法

       2.复制目录结构的复制方法

      3. 递归源目标文件的方法

      4. 整合
%title插图%num

%title插图%num

%title插图%num

【Android】拷贝文件到另一个目录下

%title插图%num %title插图%num

PS:

拷贝assets目录下文件

InputStream is = ctx.getAssets().open(“test.apk”);

特别感谢jqj1107提的建议,写代码时要谨慎,尽可能不使用try/catch,拷贝文件时检查文件属性等参数,确保万无一失

if (!oldfile.exists()) {return ;}
if (!oldfile.isFile()) {return ;}
if (!oldfile.canRead()) {return ;}

————————————————
评论:
zisuchen
zisuchen:在settings中想拷贝文件到data/user_de/0/中的其他应用的files文件中,要怎么拷贝怎样给权限?

a17816876003
辉度:这个就是java,不能算android

u012691505
下雨天没带雨伞:所以写 int length; 是干嘛用的?

niubitianping
SkyHandCsdn回复:是的,没有flush和close,会导致文件复制损坏,楼主在误人子弟。。

u012691505
下雨天没带雨伞回复: FileOutputStream也没有关闭

ljw910_00
ljw910_00:为什么是byte[] buffer = new byte[1444]; 不是1024求解

luofeng224
luofeng224:流没有关闭,应该有finally块吧

u013633075
假装不在乎你:程序中这段代码是没有用的:bytesum+=byteread//字节数 文件大小 并且注释也错了,也不表示文件大小,inStream.read(buffer)的返回值是IO流中实际二进制串值的大小,所以……

Etzmico
伊茨米可回复:读取的文件大小,或者说复制过去的文件大小。

zgf1991
zgf1991:首先感谢,我是用了 再个我想说一点, if (!oldfile.exists()) { //文件不存在时 对于这个是闲的蛋疼这样写,还是觉得要让直接copy你代码的人废点时间瞄瞄呢? 楼上有人指出了。为何不修改一下? 嘲讽吗? copy本就是想快点
点赞
Etzmico
伊茨米可回复:感谢了
点赞
zgf1991
zgf1991回复伊茨米可: File oldfile = new File(oldPath); if (!oldfile.exists()) { //文件不存在时 InputStream inStream = new FileInputStream(oldPath); //读入原文件 FileOutputStream fs = new FileOutputStream(newPath); byte[] buffer = new byte[1444]; 这个是你的代码吧。那我再说清楚一点吧,3、4楼都说了这个问题, lz,你的回复是觉得这个东西得我们用的时候自己再修改下吗? if (!oldfile.exists()) { 原文件不存在的时候,在这里复制吗?} 能不成这个! 不该去掉?你自己再去试试能用? 是你逗还是我逗呢? 对于 -> 下面还有人提到需要这句话你怎么没看到呢? 8楼的那段代码,是应该要。 我都看了,我还加了8楼的代码,你自己试试再说吧,验证有问题的话你把文章编辑下吧
Etzmico
伊茨米可回复:呵呵 下面还有人提到需要这句话你怎么没看到呢? 只能说你手头的项目对此判断没有需求,要么就是你没考虑全面,要么就是项目较简单,不需要考虑全面 文章是给所有需要的人看的,你不需要,不代表别人不需要,如果你实在觉得多余,删掉就是了,直接无视就好。
jqj1107
零下36度:
if (!oldfile.exists()) {return ;}
if (!oldfile.isFile()) {return ;}
if (!oldfile.canRead()) {return ;}
拷贝单个文件时,建议检查下原文件是否存在等属性,不要直接异常处理。
Etzmico
伊茨米可回复:学习了,谢谢。
yong7356
yong7356:学习了。。。。。。。。。。。。
icebounder
逝于寂寞:if (!oldfile.exists()) //文件不存在时 文件都不存在了,还去拷贝什么呢??一楼说的很正确的啊?楼主你的代码是复制的吧?
griefcola
griefcola回复:你写代码的时候,永远只写你预期的部分吗? 没有使用场景的情况下,楼主的代码是没有问题的
xh_jiayou
xh_jiayou:文件不存在的时候去拷贝,考个毛啊
Etzmico
伊茨米可回复:你只看得懂中文,看不懂代码么?
maggiccrystal_3
maggiccrystal_3:从 /mnt/usb/sda1/dragons/myres 下面showtest.txt , results文件 复制到 /data/Mytest 目录 在sd卡权限: 我都加了,但是还是不成功,
Etzmico
伊茨米可回复:你那个第二个目录系统目录吧?
maggiccrystal_3
maggiccrystal_3:hello,楼主,你*个方法我试了,不能拷贝成功
Etzmico
伊茨米可回复:纳尼。。。from where to where.

【Android】短信应用——短信信息实时获取

我们知道,只需通过代码就可以读到收件箱中的短信,发件箱中的短信;但是却没办法在短信发来的瞬间获取;如果我们在短信发来的一瞬间能得到相应的信息内容,那么我们就可以依次来展开很多应用了——也就是通过短信去远程操作一部手机。

 

如果想实时获取,就需要调用receiver了,写一个监听类,这样我们就可以实时获取短息信息了。

 

预览图:%title插图%num

 

还是来看看代码吧。

首先,我们需要创建一个监听类SMSBroadcastReceiver,让他去继承BroadcastReceiver。

再来初始化一个常量ACTION,并赋短信相关参数值。

 

android.provider.Telephony.SMS_RECEIVED
接着创建onReceive方法。

然后用getAction去监听手机短信相关动态,利用StringBuffer来保存短信信息。

 

再然后主要代码了。

%title插图%num
代码中的SMSAddress为发送短信的号码,SMSContent为短信内容。

 

要想看到是否成功获取,*简单的方法就是把这两个参数打印出来。

1|System.out.println(“发送号码:” + SMSAddress + “\n” + “短信内容:”
2|                                         + SMSContent);

不过要把他们加入for循环中,因为当新信息发来时,SMSAddress和SMSContent将被替换。

因此如果要是做应用时,也是在for循环中判断的。

*后要记得在Manifest.xml中注册监听器。

 

<receiver android:name=”cn.etzmico.SMSBroadcastReceiver”>
<intent-filter>
<action android:name=”android.provider.Telephony.SMS_RECEIVED”></action>
</intent-filter>
</receiver>

 

同时要加上权限。

 

<uses-permission android:name=”android.permission.RECEIVE_SMS”></uses-permission>
这样,我们运行程序后,只要有短信接收,SMSAddress和SMSContent就会被赋值。

 

这里顺便补充一个知识点,关于Eclipse程序的。

相信很多初学者不知道,Eclipse自带一个发短信插件,可以实现给虚拟机发送短信。这样,我们在做短信应用的时候,就不用同时启动多台虚拟机了……

如何操作呢?方法如下。

1.点击菜单栏中的 Window 窗口。

2.找到哦啊其中的 Show View 目录。

3. 选择 Other…。

%title插图%num

然后我们发现会弹出一个窗口。

%title插图%num

4,为了便于操作,我们在弹出的窗口的搜索栏中,直接输入 Emulator Control。

%title插图%num

5.点击列表中的 Emulator Control,再点OK;或者直接双击。

 

这样就出现了一个窗口,其中有很多参数。

其他的以后有机会再做介绍,我们这次至用到其中4个。

%title插图%num

如图所示,我们只需要输入对应的参数,选择需要的类型,*后点发送就可以了。

 

PS:有的人奇怪为什么灰色,没法输入,没法选择,那是因为你没有选中模拟器。这个插件只能同时给一个模拟器发送消息。关于模拟器的选择,和调用Emulator Control的方法差不多,区别只是在输入Emulator Control的时候输入 Devices 就可以了。你当前选中哪个模拟器了,就会给哪个模拟器发送消息,不需要输入模拟器号码。

吐槽:华为手机的搜索短信问题

这两天,被测试同事的华为荣耀8搞得焦头烂额,在其他手机能正常使用的Uri,在它身上却问题多多,不得不去吐槽国内定制系统…

一开始,我的搜索操作是这样的…
private void query(String keyword) {
Uri searchUri = Telephony.MmsSms.SEARCH_URI.buildUpon().appendQueryParameter(“pattern”, keyword).build();
mQueryHandler.startQuery(0, null, searchUri, null, null, null, null);
}

虽然,这只能模糊搜索信息的内容并不能根据联系人号码去查询,手中的一加、小米手机运行是没问题的,但…华为手机运行后,得到的cursor是null…

%title插图%num
好吧,既然这样,我换个Uri试试…
content://mms-sms/conversations/

先在自己手机测下,看是否可以查询成功…OK,可以,再借同事手机来跑下…结果,崩溃了,日志如下:
android.database.sqlite.SQLiteException: no such column: group_all (Sqlite code 1): , while compiling:
SELECT thread_id, address, body, date, _id FROM (
SELECT thread_id AS tid, date * 1000 AS normalized_date, NULL AS time_body, text_only, NULL AS group_id, ct_t, msg_box, v, retr_txt_cs, ct_cls, NULL AS type, st, NULL AS address, NULL AS person, tr_id, read, m_id, NULL AS body, NULL AS addr_body, m_type, network_type, locked, resp_txt, retr_st, NULL AS error_code, NULL AS group_all, NULL AS reply_path_present, sub, NULL AS risk_url_body, rr, ct_l, NULL AS status, NULL AS subject, _id, m_size, exp, sub_cs, NULL AS group_fail, sub_id, resp_st, date, date_sent, pri, NULL AS group_sent, thread_id, read_status, d_rpt, rpt_a, NULL AS is_secret, m_cls, NULL AS service_center FROM pdu WHERE ((msg_box != 3 AND (m_type = 128 OR m_type = 132 OR m_type = 130))) GROUP BY thread_id HAVING date = MAX(date) UNION SELECT thread_id AS tid, date * 1 AS normalized_date, time_body, NULL AS text_only, group_id, NULL AS ct_t, NULL AS msg_box, NULL AS v, NULL AS retr_txt_cs, NULL AS ct_cls, type, NULL AS st, address, person, NULL AS tr_id, read, NULL AS m_id, body, addr_body, NULL AS m_type, network_type, locked, NULL AS resp_txt, NULL AS retr_st, error_code, group_all, reply_path_present, NULL AS sub, risk_url_body, NULL AS rr, NULL AS ct_l, status, subject, _id, NULL AS m_size, NULL AS exp, NULL AS sub_cs, group_fail, sub_id, NULL AS resp_st, date, date_sent, NULL AS pri, group_sent, thread_id, NULL AS read_status, NULL AS d_rpt, NULL AS rpt_a, is_secret, NULL AS m_cls, service_center FROM sms_secret WHERE ((type != 3)) GROUP BY thread_id HAVING date = MAX(date)) GROUP BY tid HAVING normalized_date = MAX(normalized_date), (OS error – 2:No such file or directory)

无法找到group_all这列…就在这时,我开始强迫症了,Google了整整几小时,找到的资料少之又少,只有在GitHub的一个Issue中看到了相同问题(https://github.com/WhisperSystems/Signal-Android/issues/6198),但是并没有解决方法,好吧,好吧…

于是又试了几个Uri,但还是崩溃报Unrecognized URI!错误…
content://mms-sms/
content://mms-sms/messages/byphone

*后,实在没办法只能放弃彩信的搜索,只允许用户搜索Sms而不是Mms-Sms:
content://sms/inbox

不得不感概:兼容问题,是一个大问题啊!!!

Android基于局域网的socket通信

*近写了一个关于局域网socket通信的demo,代码和ui都很low,但是功能实现了,所以贴出来记录一下

主要流程如下

服务端:启动服务–>显示ip–>等待接收–>显示消息–>收到回复

客户端:录入ip–>输入消息–>发送消息–>收到结果

如下图

选择服务端还是客户端

%title插图%num

 

显示服务端ip

%title插图%num

客户端输入ip发送消息

%title插图%num

服务端收到消息

%title插图%num

主要代码如下:
服务端逻辑

public class ServiceActivity extends AppCompatActivity {

private TextView tv_clear;
private TextView tv_showIP;
private TextView tv_ip;
private TextView tv_msg;
private ServerSocket mServerSocket;
private Socket mSocket;
private StringBuffer sb = new StringBuffer();

@SuppressLint(“HandlerLeak”)
public Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 1){
Bundle data = msg.getData();
sb.append(data.getString(“msg”));
sb.append(“\n”);
tv_msg.setText(sb.toString());
}
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_service);
initView();
setListener();

try {
mServerSocket = new ServerSocket(1989);
} catch (IOException e) {
e.printStackTrace();
}
//启动服务线程
SocketAcceptThread socketAcceptThread = new SocketAcceptThread();
socketAcceptThread.start();
}

private void initView() {
tv_clear = (TextView) findViewById(R.id.tv_clear);
tv_showIP = (TextView) findViewById(R.id.tv_showIP);
tv_ip = (TextView) findViewById(R.id.tv_ip);
tv_msg = (TextView) findViewById(R.id.tv_msg);
}

private void setListener() {

tv_clear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sb.setLength(0);
tv_msg.setText(“”);
}
});
tv_showIP.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tv_ip.setText(NetWorkUtil.getIPAddress(ServiceActivity.this));
}
});

}

class SocketAcceptThread extends Thread{
@Override
public void run() {
try {
//等待客户端的连接,Accept会阻塞,直到建立连接,
//所以需要放在子线程中运行。
mSocket = mServerSocket.accept();
} catch (IOException e) {
e.printStackTrace();
Log.e(“info”, “run: ==============”+”accept error” );
return;
}
Log.e(“info”, “accept success==================”);
//启动消息接收线程
startReader(mSocket);
}
}

/**
* 从参数的Socket里获取*新的消息
*/
private void startReader(final Socket socket) {

new Thread(){
@Override
public void run() {
DataInputStream reader;
try {
// 获取读取流
reader = new DataInputStream(socket.getInputStream());
while (true) {
System.out.println(“*等待客户端输入*”);
// 读取数据
String msg = reader.readUTF();
System.out.println(“获取到客户端的信息:=” + msg);

//告知客户端消息收到
DataOutputStream writer = new DataOutputStream(mSocket.getOutputStream());
writer.writeUTF(“收到:” + msg); // 写一个UTF-8的信息

//发消息更新UI
Message message = new Message();
message.what = 1;
Bundle bundle = new Bundle();
bundle.putString(“msg”, msg);
message.setData(bundle);
handler.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}

@Override
protected void onDestroy() {
if (mServerSocket != null){
try {
mServerSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
super.onDestroy();
}
}
服务端UI

<LinearLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”
android:background=”#ffffff”
tools:context=”com.xiaoxiao9575.socketapplication.ServiceActivity”>

<LinearLayout
android:layout_width=”match_parent”
android:layout_height=”wrap_content”>
<TextView
android:id=”@+id/tv_showIP”
android:layout_width=”0dp”
android:layout_height=”wrap_content”
android:layout_weight=”1″
android:text=”显示IP”
android:gravity=”center”
android:padding=”15dp”
android:background=”#dddddd”/>
<TextView
android:id=”@+id/tv_clear”
android:layout_width=”0dp”
android:layout_height=”wrap_content”
android:layout_weight=”1″
android:text=”清除屏幕”
android:gravity=”center”
android:padding=”15dp”
android:background=”#dddddd”
android:layout_marginLeft=”5dp”/>
</LinearLayout>

<TextView
android:id=”@+id/tv_ip”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_margin=”10dp”/>

<TextView
android:id=”@+id/tv_msg”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_marginTop=”40dp”
android:layout_margin=”10dp”/>
</LinearLayout>
客户端逻辑

public class ClientActivity extends AppCompatActivity {

private EditText et_ip;
private EditText et_msg;
private TextView tv_send;
private TextView tv_confirm;

private Socket mSocket;
private OutputStream mOutStream;
private InputStream mInStream;
private SocketConnectThread socketConnectThread;
private StringBuffer sb = new StringBuffer();

@SuppressLint(“HandlerLeak”)
public Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 1){
Bundle data = msg.getData();
sb.append(data.getString(“msg”));
sb.append(“\n”);
tv_msg.setText(sb.toString());
}
}
};
private TextView tv_msg;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_client);
socketConnectThread = new SocketConnectThread();
initView();
setListener();
}

private void initView() {
et_ip = (EditText) findViewById(R.id.et_ip);
et_msg = (EditText) findViewById(R.id.et_msg);
tv_send = (TextView) findViewById(R.id.tv_send);
tv_confirm = (TextView) findViewById(R.id.tv_confirm);
tv_msg = (TextView) findViewById(R.id.tv_msg);
}

private void setListener() {
tv_send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
send(et_msg.getText().toString());
}
});
tv_confirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
socketConnectThread.start();
}
});
}

class SocketConnectThread extends Thread{
public void run(){
Log.e(“info”, “run: ============线程启动” );
try {
//指定ip地址和端口号
mSocket = new Socket(et_ip.getText().toString(), 1989);
if(mSocket != null){
//获取输出流、输入流
mOutStream = mSocket.getOutputStream();
mInStream = mSocket.getInputStream();
}else {
Log.e(“info”, “run: =========scoket==null”);
}
} catch (Exception e) {
e.printStackTrace();
return;
}
Log.e(“info”,”connect success========================================”);
startReader(mSocket);
}

}

public void send(final String str) {
if (str.length() == 0){
return;
}
new Thread() {
@Override
public void run() {
try {
// socket.getInputStream()
DataOutputStream writer = new DataOutputStream(mSocket.getOutputStream());
writer.writeUTF(str); // 写一个UTF-8的信息
System.out.println(“发送消息”);
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}

/**
* 从参数的Socket里获取*新的消息
*/
private void startReader(final Socket socket) {

new Thread(){
@Override
public void run() {
DataInputStream reader;
try {
// 获取读取流
reader = new DataInputStream(socket.getInputStream());
while (true) {
System.out.println(“*等待客户端输入*”);
// 读取数据
String msg = reader.readUTF();
System.out.println(“获取到客户端的信息:=” + msg);
Message message = new Message();
message.what = 1;
Bundle bundle = new Bundle();
bundle.putString(“msg”, msg);
message.setData(bundle);
handler.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
客户端UI

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”
tools:context=”com.xiaoxiao9575.socketapplication.ClientActivity”>

<EditText
android:id=”@+id/et_ip”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:hint=”ip”
/>
<TextView
android:id=”@+id/tv_confirm”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:text=”confirm”
android:padding=”20dp”
android:gravity=”center”
android:background=”#dddddd”/>
<EditText
android:id=”@+id/et_msg”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:hint=”msg”/>
<TextView
android:id=”@+id/tv_send”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:text=”send”
android:padding=”20dp”
android:gravity=”center”
android:background=”#dddddd”/>
<TextView
android:id=”@+id/tv_msg”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
/>
</LinearLayout>
获取ip代码

public static String getIPAddress(Context context) {
NetworkInfo info = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
if (info != null && info.isConnected()) {
if (info.getType() == ConnectivityManager.TYPE_MOBILE) {//当前使用2G/3G/4G网络
try {
//Enumeration<NetworkInterface> en=NetworkInterface.getNetworkInterfaces();
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
NetworkInterface intf = en.nextElement();
for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
return inetAddress.getHostAddress();
}
}
}
} catch (SocketException e) {
e.printStackTrace();
}

} else if (info.getType() == ConnectivityManager.TYPE_WIFI) {//当前使用无线网络
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
String ipAddress = intIP2StringIP(wifiInfo.getIpAddress());//得到IPV4地址
return ipAddress;
}
} else {
//当前无网络连接,请在设置中打开网络
}
return null;
}

/**
* 将得到的int类型的IP转换为String类型
*
* @param ip
* @return
*/
public static String intIP2StringIP(int ip) {
return (ip & 0xFF) + “.” +
((ip >> 8) & 0xFF) + “.” +
((ip >> 16) & 0xFF) + “.” +
(ip >> 24 & 0xFF);
}
权限

<!–允许应用程序改变网络状态–>
<uses-permission android:name=”android.permission.CHANGE_NETWORK_STATE”/>
<!–允许应用程序改变WIFI连接状态–>
<uses-permission android:name=”android.permission.CHANGE_WIFI_STATE”/>
<!–允许应用程序访问有关的网络信息–>
<uses-permission android:name=”android.permission.ACCESS_NETWORK_STATE”/>
<!–允许应用程序访问WIFI网卡的网络信息–>
<uses-permission android:name=”android.permission.ACCESS_WIFI_STATE”/>
<!–允许应用程序完全使用网络–>
<uses-permission android:name=”android.permission.INTERNET”/>

Android端实现多人音视频聊天应用(二)

本篇主要讨论如何使用 Agora SDK 进行多人聊天。主要需要实现以下功能:

  1. 上一篇已经实现过的聊天功能
  2. 随着加入人数和他们的手机摄像头分辨率的变化,显示不同的UI,即所谓的“分屏”
  3. 点击分屏中的小窗,可以放大显示该聊天窗

分屏

根据前期技术调研,分屏显示*好的方式是采用瀑布流结合动态聊天窗实现,这样比较方便的能够适应UI的变化。所谓瀑布流,就是目前比较流行的一种列表布局,会在界面上呈现参差不齐的多栏布局。我们先实现一个瀑布流:

瀑布流的实现方式很多,本文采用结合 GridLayoutManager的RecyclerView 来实现。我们首先自定义一个 RecyclerView,命名为 GridVideoViewContainer。核心代码如下:

  1. int count = uids.size();
  2. if (count <= 2) {
  3. // 只有本地视频或聊天室内只有另外一个人
  4. this.setLayoutManager(new LinearLayoutManager(activity.getApplicationContext(), orientation, false));
  5. } else if (count > 2) {
  6. // 多人聊天室
  7. int itemSpanCount = getNearestSqrt(count);
  8. this.setLayoutManager(new GridLayoutManager(activity.getApplicationContext(), itemSpanCount, orientation, false));
  9. }
  10. 复制代码

根据上面的代码可以看出,在聊天室里只有自己的本地视频或者只有另外一个人的时候,采用 LinearLayoutManager,这样的布局其实与前文的一对一聊天类似;而在真正意义的多人聊天室里,则采用 GridLayoutManager 实现瀑布流,其中 itemSpanCount 就是瀑布流的列数。

有了一个可用的瀑布流之后,下面我们就可以实现动态聊天窗了: 动态聊天窗的要点在于 item 的大小由视频的宽高比决定,因此 Adapter 及其对应的 layout 就该注意不要写死尺寸。在 Adapter 里控制 item 具体尺寸的代码如下:

  1. if (force || mItemWidth == 0 || mItemHeight == 0) {
  2. WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
  3. DisplayMetrics outMetrics = new DisplayMetrics();
  4. windowManager.getDefaultDisplay().getMetrics(outMetrics);
  5. int count = uids.size();
  6. int DividerX = 1;
  7. int DividerY = 1;
  8. if (count == 2) {
  9. DividerY = 2;
  10. } else if (count >= 3) {
  11. DividerX = getNearestSqrt(count);
  12. DividerY = (int) Math.ceil(count * 1.f / DividerX);
  13. }
  14. int width = outMetrics.widthPixels;
  15. int height = outMetrics.heightPixels;
  16. if (width > height) {
  17. mItemWidth = width / DividerY;
  18. mItemHeight = height / DividerX;
  19. } else {
  20. mItemWidth = width / DividerX;
  21. mItemHeight = height / DividerY;
  22. }
  23. }
  24. 复制代码

以上代码根据视频的数量确定了列数和行数,然后根据列数和屏幕宽度确定了视频的宽度,接着根据视频的宽高比和视频宽度确定了视频高度。同时也考虑了手机的横竖屏情况(就是if (width > height)这行代码)。

该 Adapter 对应的 layout 的代码如下:

  1. <RelativeLayout
  2. xmlns:android=“http://schemas.android.com/apk/res/android”
  3. android:id=“@+id/user_control_mask”
  4. android:layout_width=“match_parent”
  5. android:layout_height=“match_parent”
  6. android:orientation=“vertical”>
  7. <ImageView
  8. android:id=“@+id/default_avatar”
  9. android:layout_width=“wrap_content”
  10. android:layout_height=“wrap_content”
  11. android:layout_centerInParent=“true”
  12. android:visibility=“gone”
  13. android:src=“@drawable/icon_default_avatar”
  14. android:contentDescription=“DEFAULT_AVATAR” />
  15. <ImageView
  16. android:id=“@+id/indicator”
  17. android:layout_width=“wrap_content”
  18. android:layout_height=“wrap_content”
  19. android:layout_centerHorizontal=“true”
  20. android:layout_alignParentBottom=“true”
  21. android:layout_marginBottom=“@dimen/video_indicator_bottom_margin”
  22. android:contentDescription=“VIDEO_INDICATOR” />
  23. <LinearLayout
  24. android:id=“@+id/video_info_container”
  25. android:layout_width=“wrap_content”
  26. android:layout_height=“wrap_content”
  27. android:layout_alignParentTop=“true”
  28. android:layout_marginTop=“24dp”
  29. android:layout_marginStart=“15dp”
  30. android:layout_marginLeft=“15dp”
  31. android:visibility=“gone”
  32. android:orientation=“vertical”>
  33. <TextView
  34. android:id=“@+id/video_info_metadata”
  35. android:layout_width=“wrap_content”
  36. android:layout_height=“wrap_content”
  37. android:singleLine=“true”
  38. style=“@style/NotificationUIText” />
  39. </LinearLayout>
  40. </RelativeLayout>
  41. 复制代码

我们可以看到,layout 中有关尺寸的属性都 是wrap_content,这就使得 item 大小随视频宽高比变化成为可能。

把分屏的布局写好之后,我们就可以在每一个 item 上播放聊天视频了。

播放聊天视频

在 Agora SDK 中一个远程视频的显示只和该用户的 UID 有关,所以使用的数据源只需要简单定义为包含 UID 和对应的 SurfaceView 即可,就像这样:

  1. private final HashMap<Integer, SurfaceView> mUidsList = new HashMap<>();
  2. 复制代码

每当有人加入了我们的聊天频道,都会触发onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed)方法,*个 uid 就是他们的 UID;接下来我们要为每个 item 新建一个 SurfaceView 并为其创建渲染视图,*后将它们加入刚才创建好的mUidsList里并调用setupRemoteVideo( VideoCanvas remote )方法播放这个聊天视频。这个过程的完整代码如下:

  1. @Override
  2. public void onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) {
  3. doRenderRemoteUi(uid);
  4. }
  5. private void doRenderRemoteUi(final int uid) {
  6. runOnUiThread(new Runnable() {
  7. @Override
  8. public void run() {
  9. if (isFinishing()) {
  10. return;
  11. }
  12. if (mUidsList.containsKey(uid)) {
  13. return;
  14. }
  15. SurfaceView surfaceV = RtcEngine.CreateRendererView(getApplicationContext());
  16. mUidsList.put(uid, surfaceV);
  17. boolean useDefaultLayout = mLayoutType == LAYOUT_TYPE_DEFAULT;
  18. surfaceV.setZOrderOnTop(true);
  19. surfaceV.setZOrderMediaOverlay(true);
  20. rtcEngine().setupRemoteVideo(new VideoCanvas(surfaceV, VideoCanvas.RENDER_MODE_HIDDEN, uid));
  21. if (useDefaultLayout) {
  22. log.debug(“doRenderRemoteUi LAYOUT_TYPE_DEFAULT “ + (uid & 0xFFFFFFFFL));
  23. switchToDefaultVideoView();
  24. } else {
  25. int bigBgUid = mSmallVideoViewAdapter == null ? uid : mSmallVideoViewAdapter.getExceptedUid();
  26. log.debug(“doRenderRemoteUi LAYOUT_TYPE_SMALL “ + (uid & 0xFFFFFFFFL) + ” “ + (bigBgUid & 0xFFFFFFFFL));
  27. switchToSmallVideoView(bigBgUid);
  28. }
  29. }
  30. });
  31. }
  32. 复制代码

以上代码与前文中播放一对一视频的代码如出一撤,但是细心的读者可能已经发现我们并没有将生成的 SurfaceView 放在界面里,这正是与一对一视频的不同之处:我们要在一个抽象的 VideoViewAdapter 类里将 SurfaceView 放出来,关键代码如下:

  1. SurfaceView target = user.mView;
  2. VideoViewAdapterUtil.stripView(target);
  3. holderView.addView(target, 0, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
  4. 复制代码

一般 Android 工程师看见 holderView 就明白这是 ViewHolder 的 layout 的根 layout 了,而 user 是哪儿来的,详见文末的代码,文中不做赘述。

这样在多人聊天的时候我们就能使用分屏的方式播放用户聊天视频了,如果想放大某一个用户的视频该怎么办呢?

全屏和小窗

当用户双击某一个 item 的时候,他希望对应的视频能够全屏显示,而其他的视频则变成小窗口,那么我们先定义一个双击事件接口:

  1. public interface VideoViewEventListener {
  2. void onItemDoubleClick(View v, Object item);
  3. }
  4. 具体实现方式如下:
  5. mGridVideoViewContainer.setItemEventHandler(new VideoViewEventListener() {
  6. @Override
  7. public void onItemDoubleClick(View v, Object item) {
  8. log.debug(“onItemDoubleClick “ + v + ” “ + item + ” “ + mLayoutType);
  9. if (mUidsList.size() < 2) {
  10. return;
  11. }
  12. UserStatusData user = (UserStatusData) item;
  13. int uid = (user.mUid == 0) ? config().mUid : user.mUid;
  14. if (mLayoutType == LAYOUT_TYPE_DEFAULT && mUidsList.size() != 1) {
  15. switchToSmallVideoView(uid);
  16. } else {
  17. switchToDefaultVideoView();
  18. }
  19. }
  20. });
  21. 复制代码

将被选中的视频全屏播放的方法很容易理解,我们只看生成小窗列表的方法:

  1. private void switchToSmallVideoView(int bigBgUid) {
  2. HashMap<Integer, SurfaceView> slice = new HashMap<>(1);
  3. slice.put(bigBgUid, mUidsList.get(bigBgUid));
  4. Iterator<SurfaceView> iterator = mUidsList.values().iterator();
  5. while (iterator.hasNext()) {
  6. SurfaceView s = iterator.next();
  7. s.setZOrderOnTop(true);
  8. s.setZOrderMediaOverlay(true);
  9. }
  10. mUidsList.get(bigBgUid).setZOrderOnTop(false);
  11. mUidsList.get(bigBgUid).setZOrderMediaOverlay(false);
  12. mGridVideoViewContainer.initViewContainer(this, bigBgUid, slice, mIsLandscape);
  13. bindToSmallVideoView(bigBgUid);
  14. mLayoutType = LAYOUT_TYPE_SMALL;
  15. requestRemoteStreamType(mUidsList.size());
  16. }
  17. 复制代码

小窗列表要注意移除全屏的那个 UID,此外一切都和正常瀑布流视图相同,包括双击小窗的item将其全屏播放。

到了这里我们就已经使用 Agora SDK 完成了一个有基本功能的简单多人聊天 demo,要产品化还有很多的东西要做,在这里先做一个简单的总结吧!

总结

声网Agora 提供了高质量的视频通信 SDK,不仅覆盖了主流的操作系统,集成效率也比较高,而且还支持包括聊天,会议,直播等功能在内的多个模式的视频通话。SDK 中 API 设计基本能够满足大部分的开发需要,而且隐藏了底层开发,只需要提供 SurfaceView 和 UID 即可播放视频,这样对于 App 层的开发者来说十分友好。非常适合有视频聊天开发需求的开发者。在视频领域创业大爆发的今天,建议更多的想要从事该领域的开发者可以尝试下。

相关资源:AndroidAndroid屏幕共享共享你的屏幕和音频到另一台手机

Android端实现多人音视频聊天应用(一)

声网Agora.io的SDK让App和网站都可以实现高质量的音频通话、视频通话、全互动直播。我试着通过该SDK实现一个多人视频通话应用。本文先分享集成与一对一视频通话的部分。

环境

声网Agora.io SDK的兼容性良好,对硬件设备和软件系统的要求不高,开发环境和测试环境满足以下条件即可:

  • Android SDK API Level >= 16
  • Android Studio 2.0 或以上版本
  • 支持语音和视频功能的真机
  • App 要求 Android 4.1 或以上设备

以下是我试用声网Agora.io SDK的开发环境和测试环境:

  • 开发环境
  • Windows 10 家庭中文版
  • Java Version SE 8
  • Android Studio 3.2 Canary 4

测试环境

  • Samsung Nexus (Android 4.4.2 API 19)
  • Mi Note 3 (Android 7.1.1 API 25)

集成

步骤一:首先点此下载完整的SDK和官方demo

步骤二:既然我们要把声网Agora.io集成到自己的项目里,所以不必运行sample,我们自己新建一个HelloAgora项目,注意一定要支持C++哦。

步骤三:把libs文件夹里的arm64-v8a、、armeabi-v7a以及x86文件夹复制粘贴到app module的libs里。如果有NDK开发的必要,则把libs->include文件夹里的两个.h头文件复制粘贴到合适位置。

步骤四:首先在app module的build.gradle文件的android代码块中添加如下代码:

  1. sourceSets {
  2. main {
  3. jniLibs.srcDirs = [‘../../../libs’]
  4. }
  5. }
  6. 复制代码

然后在app module的build.gradle文件的android->defaultConfig代码块中添加如下代码:

  1. ndk {
  2. abiFilters “armeabi-v7a”, “x86”
  3. }
  4. 复制代码

接下来在app module的build.gradle文件的dependencies代码块中添加如下代码:

  1. compile ‘io.agora.rtc:full-sdk:2.0.0’
  2. 复制代码

如果用复制粘贴jar的方式,那么此处添加如下代码:

  1. compile fileTree(dir: ‘../../../libs’, include: [‘*.jar’])
  2. 复制代码

如果有自定义NDK的必要,可以继续在app module的build.gradle文件的android代码块中添加如下代码:

  1. externalNativeBuild {
  2. ndkBuild {
  3. path ‘src/main/cpp/Android.mk’
  4. }
  5. }
  6. 复制代码

然后在app module的build.gradle文件的android->defaultConfig代码块中添加如下代码:

  1. externalNativeBuild {
  2. ndkBuild {
  3. arguments “NDK_APPLICATION_MK:=src/main/cpp/Application.mk”
  4. }
  5. }
  6. 复制代码

*后sync一下,声网Agora.io的SDK就集成到项目中来了。

权限

SDK集成完毕后,为了保证SDK能正常运行,我们需要在AndroidManisfest.xml 文件中声明以下权限:

  1. <!–允许程序连接网络–>
  2. <uses-permission android:name=“android.permission.INTERNET” />
  3. <!–允许程序录制音频–>
  4. <uses-permission android:name=“android.permission.RECORD_AUDIO” />
  5. <!–允许程序使用照相设备–>
  6. <uses-permission android:name=“android.permission.CAMERA” />
  7. <!–允许程序修改全局音频设置–>
  8. <uses-permission android:name=“android.permission.MODIFY_AUDIO_SETTINGS” />
  9. <!–允许程序获取网络状态–>
  10. <uses-permission android:name=“android.permission.ACCESS_NETWORK_STATE” />
  11. <!–允许对存储空间进行读写–>
  12. <uses-permission android:name=“android.permission.WRITE_EXTERNAL_STORAGE” />
  13. <!–允许程序连接到已配对的蓝牙设备–>
  14. <uses-permission android:name=“android.permission.BLUETOOTH” />
  15. 复制代码

这些权限都是Android开发过程中的常见权限,有经验的程序员都会感觉眼熟,WRITE_EXTERNAL_STORAGE等敏感权限适配Android 6.0以后版本的问题并非本文关注重点,在此不做赘述。

混淆代码

集成SDK并声明了权限后,就该考虑混淆的问题了,我们需要在project的proguard-rules.pro文件里添加以下代码:

  1. -keep class io.agora.**{*;}
  2. 复制代码

经过以上过程后,我们已经完成了声网Agora.io SDK的快速集成,迈出了走向连麦直播、在线抓娃娃、直播问答、远程狼人杀等风口的*步。在接下来的文章里,我将继续分享APP ID鉴权、Token鉴权、一对一视频聊天、创建群聊room、分屏、窗口切换和前后摄像头切换等内容。

鉴权

APP ID鉴权

所谓APP ID,就是在 Agora创建每个项目都有的一个唯一标识。App ID 可以明确你的项目及组织身份,并在 joinChannel 方法中作为参数,连接到 Agora 实时网络中,实现实时通信或直播功能。不同的App ID在Agora实时网络中的通话是完全隔离的;Agora 提供的频道信息、计费、管理服务也都是基于 App ID。

申请APP ID的操作很简便,只要在Agora官网https://dashboard.agora.io/projects右侧栏目的“项目”中点击“添加新项目”,只需输入项目名就可生成APP ID,全过程如下图所示:

找到,把“<#YOUR APP ID#>”替换为图中的马赛克里的字符串。

  1. <string name=“agora_app_id”><#YOUR APP ID#></string>
  2. 复制代码

以上就是APP ID鉴权的全过程。

尽管App ID鉴权在*大程度上方便了开发者使用 Agora 的服务。但App ID 鉴权的安全性不佳,一旦有别有用心的人非法获取了你的 App ID,他就可以在 Agora 提供的SDK中使用你的App ID。如果你的项目对安全性要求高,或者增加用户权限设置的话,建议采用Token鉴权。

Token鉴权

在通信和直播场景中存在着多个角色,而每种角色又对应着一些默认权限。比如在直播场景中,主播可以发布流、订阅流、邀请嘉宾;观众可以订阅流、申请连麦;管理员则可以踢人或禁言。

Token鉴权的步骤比APP ID鉴权稍微复杂一些,在上文项目列表中查看 App ID 的地方,启用该项目的 App Certificate:

首先,点击激活项目右上方的 编辑 按钮。

将你的 App Certificate 保存在服务器端,且对任何客户端均不可见。当项目的 App Certificate 被启用后,你必须使用 Token。例如: 在启用 App Certificate 前,你可以使用 App ID 加入频道。但启用了 App Certificate 后,你就必须使用 Token 加入频道。后台如何用App Certificate生成Token本文不做赘述。

初始化Agora

RtcEngine 类包含应用程序调用的主要方法,调用 RtcEngine 的接口*好在同一个线程进行,不建议在不同的线程同时调用。

目前 Agora Native SDK 只支持一个 RtcEngine 实例,每个应用程序仅创建一个 RtcEngine 对象 。 RtcEngine 类的所有接口函数,如无特殊说明,都是异步调用,对接口的调用建议在同一个线程进行。所有返回值为 int 型的 API,如无特殊说明,返回值 0 为调用成功,返回值小于 0 为调用失败。

IRtcEngineEventHandler接口类用于SDK向应用程序发送回调事件通知,应用程序通过继承该接口类的方法获取 SDK 的事件通知。

接口类的所有方法都有缺省(空)实现,应用程序可以根据需要只继承关心的事件。在回调方法中,应用程序不应该做耗时或者调用可能会引起阻塞的 API(如 SendMessage),否则可能影响 SDK 的运行。

  1. private RtcEngine mRtcEngine;
  2. /**
  3. * Tutorial Step 1
  4. * 初始化Agora,创建 RtcEngine 对象
  5. */
  6. private void initializeAgoraEngine() {
  7. try {
  8. mRtcEngine = RtcEngine.create(getBaseContext(), getString(R.string.agora_app_id), mRtcEventHandler);
  9. } catch (Exception e) {
  10. Log.e(LOG_TAG, Log.getStackTraceString(e));
  11. throw new RuntimeException(“Agora初始化失败了,检查一下是哪儿出错了\n” + Log.getStackTraceString(e));
  12. }
  13. }
  14. private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
  15. @Override
  16. public void onFirstRemoteVideoDecoded(final int uid, int width, int height, int elapsed) {
  17. runOnUiThread(new Runnable() {
  18. @Override
  19. public void run() {
  20. //设置远端视频显示属性
  21. setupRemoteVideo(uid);
  22. }
  23. });
  24. }
  25. @Override
  26. public void onUserOffline(int uid, int reason) {
  27. runOnUiThread(new Runnable() {
  28. @Override
  29. public void run() {
  30. //其他用户离开当前频道回调
  31. onRemoteUserLeft();
  32. }
  33. });
  34. }
  35. @Override
  36. public void onUserMuteVideo(final int uid, final boolean muted) {
  37. runOnUiThread(new Runnable() {
  38. @Override
  39. public void run() {
  40. //其他用户已停发/已重发视频流回调
  41. onRemoteUserVideoMuted(uid, muted);
  42. }
  43. });
  44. }
  45. };
  46. private void onRemoteUserLeft() {
  47. FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);
  48. container.removeAllViews();
  49. //文案可随意定制
  50. View tipMsg = findViewById(R.id.quick_tips_when_use_agora_sdk);
  51. tipMsg.setVisibility(View.VISIBLE);
  52. }
  53. private void onRemoteUserVideoMuted(int uid, boolean muted) {
  54. FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);
  55. SurfaceView surfaceView = (SurfaceView) container.getChildAt(0);
  56. Object tag = surfaceView.getTag();
  57. if (tag != null && (Integer) tag == uid) {
  58. surfaceView.setVisibility(muted ? View.GONE : View.VISIBLE);
  59. }
  60. }
  61. 复制代码

打开视频模式

enableVideo()方法用于打开视频模式。可以在加入频道前或者通话中调用,在加入频道前调用,则自动开启视频模式,在通话中调用则由音频模式切换为视频模式。调用 disableVideo() 方法可关闭视频模式。

setVideoProfile()方法设置视频编码属性(Profile)。每个属性对应一套视频参数,如分辨率、帧率、码率等。 当设备的摄像头不支持指定的分辨率时,SDK 会自动选择一个合适的摄像头分辨率,但是编码分辨率仍然用 setVideoProfile() 指定的。

该方法仅设置编码器编出的码流属性,可能跟*终显示的属性不一致,例如编码码流分辨率为 640×480,码流的旋转属性为 90 度,则显示出来的分辨率为竖屏模式。

  1. /**
  2. * Tutorial Step 2
  3. * 打开视频模式,并设置本地视频属性
  4. */
  5. private void setupVideoProfile() {
  6. //打开视频模式
  7. mRtcEngine.enableVideo();
  8. //设置本地视频属性
  9. mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_360P, false);
  10. }
  11. 复制代码

设置本地视频显示属性

setupLocalVideo( VideoCanvas local )方法用于设置本地视频显示信息。应用程序通过调用此接口绑定本地视频流的显示视窗(view),并设置视频显示模式。 在应用程序开发中,通常在初始化后调用该方法进行本地视频设置,然后再加入频道。退出频道后,绑定仍然有效,如果需要解除绑定,可以调用 setupLocalVideo(null) 。

  1. /**
  2. * Tutorial Step 3
  3. * 设置本地视频显示属性
  4. */
  5. private void setupLocalVideo() {
  6. FrameLayout container = (FrameLayout) findViewById(R.id.local_video_view_container);
  7. SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
  8. surfaceView.setZOrderMediaOverlay(true);
  9. container.addView(surfaceView);
  10. mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_ADAPTIVE, 0));
  11. }
  12. 复制代码

加入一个频道

joinChannel(String token,String channelName,String optionalInfo,int optionalUid )方法让用户加入通话频道,在同一个频道内的用户可以互相通话,多个用户加入同一个频道,可以群聊。 使用不同 App ID 的应用程序是不能互通的。如果已在通话中,用户必须调用 leaveChannel() 退出当前通话,才能进入下一个频道。

  1. /**
  2. * Tutorial Step 4
  3. * 加入一个频道
  4. */
  5. private void joinChannel() {
  6. //如果不指定UID,Agroa将自动生成并分配一个UID
  7. mRtcEngine.joinChannel(null, “demoChannel1”, “Extra Optional Data”, 0);
  8. }
  9. 复制代码

设置远端视频显示属性

setupRemoteVideo( VideoCanvas remote)方法用于绑定远程用户和显示视图,即设定 uid 指定的用户用哪个视图显示。调用该接口时需要指定远程视频的 uid,一般可以在进频道前提前设置好。

如果应用程序不能事先知道对方的 uid,可以在 APP 收到 onUserJoined 事件时设置。如果启用了视频录制功能,视频录制服务会做为一个哑客户端加入频道,因此其他客户端也会收到它的 onUserJoined 事件,APP 不应给它绑定视图(因为它不会发送视频流),如果 APP 不能识别哑客户端,可以在 onFirstRemoteVideoDecoded 事件时再绑定视图。解除某个用户的绑定视图可以把 view 设置为空。退出频道后,SDK 会把远程用户的绑定关系清除掉。

  1. /**
  2. * Tutorial Step 5
  3. * 设置远端视频显示属性
  4. */
  5. private void setupRemoteVideo(int uid) {
  6. FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);
  7. if (container.getChildCount() >= 1) {
  8. return;
  9. }
  10. SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
  11. container.addView(surfaceView);
  12. mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_ADAPTIVE, uid));
  13. surfaceView.setTag(uid);
  14. //文案可随意定制
  15. View tipMsg = findViewById(R.id.quick_tips_when_use_agora_sdk);
  16. tipMsg.setVisibility(View.GONE);
  17. }
  18. 复制代码

离开当前频道

leaveChannel()方法用于离开频道,即挂断或退出通话。

当调用 joinChannel() API 方法后,必须调用 leaveChannel() 结束通话,否则无法开始下一次通话。 不管当前是否在通话中,都可以调用 leaveChannel(),没有副作用。该方法会把会话相关的所有资源释放掉。该方法是异步操作,调用返回时并没有真正退出频道。在真正退出频道后,SDK 会触发 onLeaveChannel 回调。

  1. /**
  2. * Tutorial Step 6
  3. * 离开当前频道
  4. */
  5. private void leaveChannel() {
  6. mRtcEngine.leaveChannel();
  7. }
  8. public void onEncCallClicked(View view) {
  9. finish();
  10. }
  11. @Override
  12. protected void onDestroy() {
  13. super.onDestroy();
  14. leaveChannel();
  15. RtcEngine.destroy();
  16. mRtcEngine = null;
  17. }
  18. 复制代码

管理摄像头

switchCamera()方法用于在前置/后置摄像头间切换。除此以外Agora还提供了一下管理摄像头的方法:例如setCameraTorchOn(boolean isOn)设置是否打开闪光灯、setCameraAutoFocusFaceModeEnabled(boolean enabled)设置是否开启人脸对焦功能等等。

  1. /**
  2. * Tutorial Step 7
  3. * 切换前置/后置摄像头
  4. */
  5. public void onSwitchCameraClicked(View view) {
  6. mRtcEngine.switchCamera();
  7. }
  8. 复制代码

将自己静音

muteLocalAudioStream(boolean muted)方法用于静音/取消静音。该方法可以允许/禁止往网络发送本地音频流。但该方法并没有禁用麦克风,不影响录音状态。

  1. /**
  2. * Tutorial Step 8
  3. * 将自己静音
  4. */
  5. public void onLocalAudioMuteClicked(View view) {
  6. ImageView iv = (ImageView) view;
  7. if (iv.isSelected()) {
  8. iv.setSelected(false);
  9. iv.clearColorFilter();
  10. } else {
  11. iv.setSelected(true);
  12. iv.setColorFilter(getResources().getColor(R.color.colorPrimary), PorterDuff.Mode.MULTIPLY);
  13. }
  14. mRtcEngine.muteLocalAudioStream(iv.isSelected());
  15. }
  16. 复制代码

暂停本地视频流

muteLocalVideoStream(boolean muted)方法用于暂停发送本地视频流,但该方法并没有禁用摄像头,不影响本地视频流获取。

  1. /**
  2. * Tutorial Step 9
  3. * 暂停本地视频流
  4. */
  5. public void onLocalVideoMuteClicked(View view) {
  6. ImageView iv = (ImageView) view;
  7. if (iv.isSelected()) {
  8. iv.setSelected(false);
  9. iv.clearColorFilter();
  10. } else {
  11. iv.setSelected(true);
  12. iv.setColorFilter(getResources().getColor(R.color.colorPrimary), PorterDuff.Mode.MULTIPLY);
  13. }
  14. mRtcEngine.muteLocalVideoStream(iv.isSelected());
  15. FrameLayout container = (FrameLayout) findViewById(R.id.local_video_view_container);
  16. SurfaceView surfaceView = (SurfaceView) container.getChildAt(0);
  17. surfaceView.setZOrderMediaOverlay(!iv.isSelected());
  18. surfaceView.setVisibility(iv.isSelected() ? View.GONE : View.VISIBLE);
  19. }
  20. 复制代码

运行效果

拿两部手机安装编译好的App,如果能看见两个自己,说明你成功了。

Android项目引入ReactNative–九步大法

很多时候我们是需要在原Android工程中添加ReactNative,而不是直接react-native init hello来创建工程,而且官网的说明不是很详细,不是完全针对安卓的,所以本文的必要性不言而喻。
#创建Android原生工程
新建Android原生工程,这里就不详细叙述了,如下图:

%title插图%num %title插图%num
点击***finish***到这里Android原生工程创建完成。

运行一下看下效果:

%title插图%num

#动态添加ReactNative
###*步:初始化package.json文件:
在工程根目录下的CMD中输入npm init,然后会生成package.json文件

%title插图%num
⚠️:这里name不能使用大写,如上动图所示,填写完相应的信息后会在根目录中生成相应的package.json文件,里面内容如下:

{
“name”: “reactnativeapp”,
“version”: “1.0.0”,
“description”: “demo”,
“main”: “index.js”,
“scripts”: {
“test”: “echo \”Error: no test specified\” && exit 1″
},
“author”: “libin”,
“license”: “ISC”
}

###第二步:在package.json文件中添加启动脚本:

“start”: “node node_modules/react-native/local-cli/cli.js start”

%title插图%num
###第三步:添加react和react-native 模块:
在根目录执行如下代码:

npm install –save react react-native

效果如图:

%title插图%num

执行完成后会出现下图的node_modules

%title插图%num

查看项目中有node_modules,说明react和react native 安装完成,如果没有说明安装失败,需要重新安装

###第四步:添加index.android.js文件到项目中:

import React from ‘react’;
import {
AppRegistry,
StyleSheet,
Text,
View
} from ‘react-native’;

class HelloWorldApp extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.hello}>Hello world! I am from ReactNattive!!</Text>
</View>
)
}
}
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: ‘center’,
},
hello: {
fontSize: 20,
textAlign: ‘center’,
margin: 10,
},
});

AppRegistry.registerComponent(‘ReactNativeApp’, () => HelloWorldApp);

%title插图%num

⚠️:AppRegistry.registerComponent(‘ReactNativeApp’, () => ReactNativeApp);
里面的名称 必须和你的工程名一致,对这个文件不熟悉的童鞋可以看本人之前的代码或者官网:

react-native官网

下图是官网相关介绍:

%title插图%num

###第五步:添加ReactNative相关依赖:

1.在app的build.gradle文件中添加react-native依赖库

compile “com.facebook.react:react-native:+”

%title插图%num
2.在project的build.gradle文件中添加react-native路径

maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url “$rootDir/../node_modules/react-native/android”
}

⚠️:这里注意不要使用maven中的,因为我们使用的是我们本地的node_modules

%title插图%num

###第六步:添加相关权限:
在AndroidManifest.xml中添加如下代码:

<uses-permission android:name=”android.permission.INTERNET” />

###第七步:添加reactnative组件:
添加com.facebook.react.ReactRootView 组件 布局代码如下

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”
android:gravity=”center”>

<TextView
android:layout_marginBottom=”50dp”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”native—–>Hello World!” />

<com.facebook.react.ReactRootView
android:id=”@+id/react_root_view”
android:layout_width=”300dp”
android:layout_height=”300dp”/>

</LinearLayout>

java代码如下:

public class MainActivity extends AppCompatActivity {
ReactRootView react_root_view ;
ReactInstanceManager mReactInstanceManager;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

react_root_view = (ReactRootView) findViewById(R.id.react_root_view);

mReactInstanceManager =ReactInstanceManager.builder()
.setApplication(getApplication())
.setBundleAssetName(“index.android.bundle”)
.setJSMainModuleName(“index.android”)
.addPackage(new MainReactPackage())
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();

//ReactNativeApp 是项目名,需要和index.adnroid.js中的保持一致
react_root_view.startReactApplication(mReactInstanceManager, “ReactNativeApp”, null);

}
}

###第八步:添加DevSettingsActivity配置

将DevSettingsActivity配置加入到AndroidManifest.xml文件中

%title插图%num
###第九步:实现ReactApplication
我们需要自定义Application然后去实现ReactApplication接口中的方法。

public class MyApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
};

@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}

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

到此,我们已经大功告成,下面来看下效果。

%title插图%num

uni-app 现在流行程度怎么样;有 UI 库推荐吗

*近要做一个简单的小程序+App,看中了 uni-app 这种一次编写生成多端的东西,现在大家都用的多吗;
另外这个有一直在维护的方便的 UI 库来使用嘛?

v2 有很多原生党,但是我就说一点,腾讯云的开源项目 discuzQ 就是用的 uniapp,流行程度你说怎么样?

但是有一说一,我虽然赞成使用 uniapp,但是 uniapp 的 ui 库不得不说,基本都是狗屎。
Rocketer Reply 3
Rocketer 15 小时 31 分钟前 via iPhone
@uqf0663 “党”在中文里多含贬义,还是不要这么说的好。

谁不喜欢省事呢?但很多人坚持原生,是为了照顾使用低配机的老人、穷人等弱势群体。他们要付出比统一框架更多的劳动时间,就为了让用户能够更流畅一点。我认为他们是应当受到尊重的。

当然每个人都可以有自己的观点,但如果不是为了明确表达对这个群体的反对意见,还是别用这些容易引起误会的词。
p0h5 Reply 4
p0h5 8 小时 38 分钟前 via iPhone ❤️ 1
uview ui
不错
seakingii Reply 5
seakingii 6 小时 58 分钟前
我*近在用 UNI-APP,是当前客户要求的.另一个客户的 APP 也是用 UNI-APP 开发的.
我个人不喜欢 UNI-APP,感觉官方的文档,提供的组件总有这样那样的问题.第三方组件也是质量不高
但,方便,跨端,简单,压倒一切.
han3sui Reply 6
han3sui 6 小时 20 分钟前
uni-app 的 ui 库的质量跟 vant 这种流行的还是没法比
rogwan Reply 7
rogwan 6 小时 5 分钟前 via Android ❤️ 1
@uqf0663 discuzq will be restructured by react on the next version
imydou Reply 8
imydou 5 小时 52 分钟前
内置 request 没有拦截器,除了跨端没任何优势
superBearL Reply 9
superBearL 5 小时 52 分钟前
uView UI 还不错
matatabi Reply 10
matatabi 5 小时 36 分钟前 via iPhone
Uniapp 不错,简单才是王道
HiCode Reply 11
HiCode 5 小时 29 分钟前
@rogwan 哪里看到的消息???
xueyangkk Reply 12
xueyangkk 1 小时 26 分钟前
实战过几个项目,说说感受吧
这要看你做的什么项目 如果是电商类的 ,交互不是那么频繁的话 我觉得是完全 ok 的 ,唯一就是打包的时候 会有一些奇怪的问题 但是查查资料 基本都能解决 。

如果做的项目有大量数据刷新 也可以用 uni-app 但是打包成 app 卡顿是避免不了的。至于生成小程序和原生的基本没差别

miui+ 的同步原理是依靠 WIFI 局域网吗?

miui+ 的同步原理是依靠 WIFI 局域网吗?

还是蓝牙?或是走公网?

15 条回复    2021-01-27 13:50:00 +08:00
Atomo
    1

Atomo   102 天前   ❤️ 1

目测是局域网,
另华为的多屏协同是蓝牙认证,拉起 wifi,走自己的 wlan 修改版协议
yayiji
    2

yayiji   102 天前

笔记我看可以直接走公网,文件照片什么的,还不太清楚
systemcall
    3

systemcall   102 天前 via Android

目测是先蓝牙配对,其中一个设备 WiFi 开个热点,通过蓝牙通知另一个来连接。之后哪怕直接走 adb 都没有问题,功能不算很复杂,有很多开源的轮子,而且华为和联想、微软*近都有类似的东西
echo314
    4

echo314   102 天前   ❤️ 1

wifi direct ?
NSAgold
    5

NSAgold   102 天前

应该是类似 scrcpy 的实现(纯猜测) *终肯定是走 wifi 蓝牙那点可怜的带宽不够的
felixlong
    6

felixlong   102 天前 via Android

@Atomo 都是蓝牙拉起 miracast 啦。各家应该没什么差别。
wysnylc
    7

wysnylc   102 天前

首先排除公网
natashahollyz
    8

natashahollyz   102 天前 via iPhone

WiFi
Atomo
    9

Atomo   101 天前

@felixlong #6 我刚刚试了华为,手机忘记 wifi 密码,电脑客户端通过蓝牙拉起手机的 wlan,可以实现多屏协同,手机始终没有连接路由器的 wifi,但必须要打开 wlan 开关
ciaoly
    10

ciaoly   101 天前 via Android

楼上说了,是 WiFi direct 和 miracast
JayFang1993
    11

JayFang1993   101 天前

有 Mac 版吗?发布会上好像只看到 Windows
ETO
    12

ETO   99 天前

我电脑手机不连 wifi 也能使用,我猜应该是手机内自己开的热点吧。
ETO
    13

ETO   99 天前

@Atomo 对的,我用的小米 10 也是这样的。
systemcall
    14

systemcall   99 天前

@ciaoly miracast 本身就是蓝牙+WiFi Direct,那个东西只是解决了无线显示和输入
传输文件、打开应用,猜测是走的网络协议,这个只要包可以发过去就可以了,哪怕走蓝牙都可以。估计会因为实现难度和实际效果,限制成走 WiFi,同局域网和 WiFi Direct 因为网络状态变化要做一些适配。公网只做一下日历、便签和照片同步,走的手机厂家自己的云服务,和手机没多大关系
@ETO 应该是用蓝牙拉起的配对,Miracast 就是支持用蓝牙拉起的,AirDrop 没记错的话也是
hao150
    15

hao150   73 天前

走的是 WIFI 。如果电脑没有 WIFI 的话是没办法使用的