第二百四十四回 如何处理ListView中的事件冲突


我们在上一章回中分享了一个好用的Json工具,本章回中将介绍 如何处理ListView中的事件冲突.闲话休提,让我们一起Talk Flutter吧。

在这里插入图片描述

1. 概念介绍

在Flutter应用开发中,ListView组件是实现滚动列表展示内容的基石。当我们在ListView的item项上添加点击跳转功能时,通常会遇到这样一个问题:如果直接使用Listener组件来监听点击事件,那么在用户试图滑动列表时,可能由于手势判断的问题导致误触发点击事件,从而不当地跳转到详情页面。这便是我们说的事件冲突。

本章回中将详细介绍如何利用GestureDetector组件巧妙地解决这一冲突,并确保ListView既能流畅滑动又能准确响应点击事件。

2. 使用方法

下面是一个示例代码,代码中构建一个包含多个可点击item项的ListView,点击Item时跳转到DetailPage,不过这里省略了DetailPage页面的内容。

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return Listener(
      onPointerDown: (_) {
        // 点击item如跳转至详情页
        Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(item: items[index])));
      },
      child: ListTile(title: Text(items[index].title)),
    );
  },
);

然而,Listener组件对于任何触摸事件都非常敏感,包括那些本应用于滑动ListView的手势。这就意味着用户在尝试滑动列表时,可能无意间触发了详情页的跳转。此时会跳转到item的详情页面。我们的经验是使用GestureDetector组件替换Listener组件,这样可以确保ListView既能流畅滑动又能准确响应点击事件。

3. 示例代码

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return GestureDetector(
      onTap: () {
        // 点击item如跳转至详情页
        Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(item: items[index])));
      },
      child: ListTile(title: Text(items[index].title)),
    );
  },
);

上面的示例代码中使用了GestureDetector组件替换了Listener组件。GestureDetector不仅能够精确地区分点击和其他手势,而且提供了更多自定义选项以适应不同场景的需求。例如,可以通过设置behavior属性来调整其与其他手势识别器之间的交互方式,比如在ScrollView内防止消耗所有手势:

GestureDetector(
  behavior: HitTestBehavior.opaque, 
  onTap: () {...},
  child: ...
)

示例代码中的behavior属性值有多种,详细的功能如下:

  • HitTestBehavior.opaque:此模式下,GestureDetector会消费它覆盖的所有手势。
  • HitTestBehavior.translucent:在此模式下,手势会被传递给子组件,同时GestureDetector也会接收到这些手势。
  • HitTestBehavior.deferToChild:在这种情况下,只有当子组件本身没有处理手势时,GestureDetector才会处理手势。

在实际项目中,我们可以根据实际需求选择合适的HitTestBehavior可以进一步优化手势识别效果。

4. 内容总结

最后,我们对本章回的内容做一个全面的总结:

  • ListView时,使用GestureDetector和Listener都可以响应item的点击事件;
  • 我们推荐使用能GestureDetector组件,它可以有效解决滑动与点击冲突问题,提供更加顺畅且符合用户预期的交互体验;
  • GestureDetector组件的behavior属性可以控制手势的使用范围,它有不同的值供我们选择;

看官们,与"如何处理ListView中的事件冲突"相关的内容就介绍到这里,欢迎大家在评论区交流与讨论!

本书是一本Android进阶类书籍,采用理论、源码和实践相结合的方式来阐述高水准的Android应用开发要点。本书从三个方面来组织内容。第一,介绍Android开发者不容易掌握的一些知识点;第二,结合Android源代码和应用层开发过程,融会贯通,介绍一些比较深入的知识点;第三,介绍一些核心技术和Android的性能优化思想。 第1章 Activity的生命周期和启动模式 1 1.1 Activity的生命周期全面分析 1 1.1.1 典型情况下的生命周期分析 2 1.1.2 异常情况下的生命周期分析 8 1.2 Activity的启动模式 16 1.2.1 Activity的LaunchMode 16 1.2.2 Activity的Flags 27 1.3 IntentFilter的匹配规则 28 第2章 IPC机制 35 2.1 Android IPC简介 35 2.2 Android中的多进程模式 36 2.2.1 开启多进程模式 36 2.2.2 多进程模式的运行机制 39 2.3 IPC基础概念介绍 42 2.3.1 Serializable接口 42 2.3.2 Parcelable接口 45 2.3.3 Binder 47 2.4 Android中的IPC方式 61 2.4.1 使用Bundle 61 2.4.2 使用文件共享 62 2.4.3 使用Messenger 65 2.4.4 使用AIDL 71 2.4.5 使用ContentProvider 91 2.4.6 使用Socket 103 2.5 Binder连接池 112 2.6 选用合适的IPC方式 121 第3章 View的事件体系 122 3.1 View基础知识 122 3.1.1 什么是View 123 3.1.2 View的位置参数 123 3.1.3 MotionEvent和TouchSlop 125 3.1.4 VelocityTracker、GestureDetector和Scroller 126 3.2 View的滑动 129 3.2.1 使用scrollTo/scrollBy 129 3.2.2 使用动画 131 3.2.3 改变布局参数 133 3.2.4 各种滑动方式的对比 133 3.3 弹性滑动 135 3.3.1 使用Scroller 136 3.3.2 通过动画 138 3.3.3 使用延时策略 139 3.4 View的事件分发机制 140 3.4.1 点击事件的传递规则 140 3.4.2 事件分发的源码解析 144 3.5 View的滑动冲突 154 3.5.1 常见的滑动冲突场景 155 3.5.2 滑动冲突处理规则 156 3.5.3 滑动冲突的解决方式 157 第4章 View的工作原理 174 4.1 初识ViewRoot和DecorView 174 4.2 理解MeasureSpec 177 4.2.1 MeasureSpec 177 4.2.2 MeasureSpec和LayoutParams的对应关系 178 4.3 View的工作流程 183 4.3.1 measure过程 183 4.3.2 layout过程 193 4.3.3 draw过程 197 4.4 自定义View 199 4.4.1 自定义View的分类 200 4.4.2 自定义View须知 201 4.4.3 自定义View示例 202 4.4.4 自定义View的思想 217 第5章 理解RemoteViews 218 5.1 RemoteViews的应用 218 5.1.1 RemoteViews在通知栏上的应用 219 5.1.2 RemoteViews在桌面小部件上的应用 221 5.1.3 PendingIntent概述 228 5.2 RemoteViews的内部机制 230 5.3 RemoteViews的意义 239 第6章 Android的Drawable 243 6.1 Drawable简介 243 6.2 Drawable的分类 244 6.2.1 BitmapDrawable 244 6.2.2 ShapeDrawable 247 6.2.3 LayerDrawable 251 6.2.4 StateListDrawable 253 6.2.5 LevelListDrawable 255 6.2.6 TransitionDrawable 256 6.2.7 Ins
开发文档:OPEN API/云直播/播放地址/播放地址接口(新)/获取播放地址 获取播放地址 接口功能: 该接口用于通过设备序列号、通道号获取单台设备的播放地址信息,无法获取永久有效期播放地址。 请求地址 https://open.ys7.com/api/lapp/v2/live/address/get 子账户token请求所需最小权限 "Permission":"Get" "Resource":"dev:序列号" 请求方式 POST 请求参数 参数名 类型 描述 是否必选 accessToken String 授权过程获取的access_token Y deviceSerial String 设备序列号例如427734222,均采用英文符号,限制最多50个字符 Y channelNo Integer 通道号,非必选,默认为1 N protocol Integer 流播放协议,1-ezopen、2-hls、3-rtmp、4-flv,默认为1 N code String ezopen协议地址的设备的视频加密密码 N expireTime Integer 过期时长,单位秒;针对hls/rtmp/flv设置有效期,相对时间;30秒-720天 N type String 地址的类型,1-预览,2-本地录像放,3-云存储录像放,非必选,默认为1;放仅支持rtmp、ezopen、flv协议 N quality Integer 视频清晰度,1-高清(主码流)、2-流畅(子码流) N startTime String 本地录像/云存储录像放开始时间,云存储开始结束时间必须在同一天,示例:2019-12-01 00:00:00 N stopTime String 本地录像/云存储录像放结束时间,云存储开始结束时间必须在同一天,示例:2019-12-01 23:59:59 N supportH265 Integer 请判断播放端是否要求播放视频为H265编码格式,1表示需要,0表示不要求 N playbackSpeed String 放倍速。倍速为 -1( 支持的最大倍速)、0.5、1、2、4、8、16; 仅支持protocol为4-flv 且 type为2-本地录像放( 部分设备可能不支持16倍速) 或者 3-云存储录像放 N gbchannel String 国标设备的通道编号,视频通道编号ID N HTTP请求报文 POST /api/lapp/v2/live/address/get HTTP/1.1 Host: open.ys7.com Content-Type: application/x-www-form-urlencoded accessToken=at.dunwhxt2azk02hcn7phqygsybbw0wv6p&deviceSerial=C78957921&channelNo=1 返数据 { "msg": "Operation succeeded", "code": "200", "data": { "id": "254708522214232064", "url": "https://open.ys7.com/v3/openlive/C78957921_1_1.m3u8?expire=1606999273&id=254708522214232064&t=093e5c6668d981e0f0b8d2593d69bdc98060407d1b2f42eaaa17a62b15ee4f99&ev=100", "expireTime": "2020-12-03 20:41:13" } } OPEN API/云直播/录像文件查询/云存储录像查询/根据时间获取存储文件信息 根据时间获取存储文件信息 接口功能: 该接口用于根据时间获取存储文件信息。注: 请求地址: https://open.ys7.com/api/lapp/video/by/time 请求方式 POST 子账户token请求所需最小权限 无 请求参数; 参数名 类型 描述 是否必选 accessToken String 访问令牌 Y deviceSerial String 设备序列号,存在英文字母的设备序列号,字母需为大写 Y channelNo int 通道号,非必选,默认为1 N startTime long 起始时间,时间格式为:1378345128000。非必选,默认为当天0点 N endTime long 结束时间,时间格式为:1378345128000。非必选,默认为当前时间 N recType int 放源,0-系统自动选择,1-云存储,2-本地录像。非必选,默认为0 N version String 返分页结构,recType=1时,传2.0会返分页结构; recType=2时,传2.0且pageSize不为空的情况才会返分页结构 N pageSize int recType为1或2时,可指定返的文件数量,云存储类型分页大小范围:1-1000,本地录像类型分页大小范围: 1-500 N HTTP请求报文 POST /api/lapp/alarm/video HTTP/1.1 Host: open.ys7.com Content-Type: application/x-www-form-urlencoded accessToken=at.dunwhxt2azk02hcn7phqygsybbw0wv6p&deviceSerial=427734203&channelNo=1&startTime=1378345128000&endTime=1378345128000&recType=0 返数据 { “code”: “200”, “msg”: “操作成功”, “data”: [ { “recType”: 0, “startTime”: 1378345128000, “endTime”: 1378345128000, “deviceSerial”: “409864662”, “cameraNo”: “1”, “localType”: “ALLEVENT”, “channelType”: “D”, “id”: 20432171600, “fileId”: “20432171600”, “ownerId”: “chenyong”, “fileType”: 1, “fileName”: “”, “cloudType”: 1, “fileIndex”: “5d5b6d94-13e8-440b-a25b-00eda521c35f”, “fileSize”: 4011828, “locked”: 0, “createTime”: “2016-08-22 13:59:13”, “crypt”: 22, “keyChecksum”: “”, “videoLong”: 150000, “coverPic”: “https://218.244.139.5:0/api/cloud?method=download&fid=a14f8348-1dd1-11b2-aef9-dbed68cc4c3e&session=hik%24shipin7%231%23USK%23at.a2rwv07y6v44ozhqblhb0tly337hb4vq-4jktv8rbjh-193fe5b-cm38stbht”, “downloadPath”: “218.244.139.5:0”, “type”: 1 }, { “recType”: 0, “startTime”: 1378345128000, “endTime”: 1378345128000, “deviceSerial”: “409864662”, “cameraNo”: “1”, “localType”: “ALLEVENT”, “channelType”: “D”, “id”: 20432171600, “fileId”: “20432171600”, “ownerId”: “chenyong”, “fileType”: 1, “fileName”: “”, “cloudType”: 1, “fileIndex”: “5d5b6d94-13e8-440b-a25b-00eda521c35f”, “fileSize”: 4011828, “locked”: 0, “createTime”: “2016-08-22 13:59:13”, “crypt”: 22, “keyChecksum”: “”, “videoLong”: 150000, “coverPic”: “https://218.244.139.5:0/api/cloud?method=download&fid=a14f8348-1dd1-11b2-aef9-dbed68cc4c3e&session=hik%24shipin7%231%23USK%23at.a2rwv07y6v44ozhqblhb0tly337hb4vq-4jktv8rbjh-193fe5b-cm38stbht”, “downloadPath”: “218.244.139.5:0”, “type”: 1 } ] } } 返数据 (分页结构返) { “msg”: “操作成功!”, “code”: “200”, “data”: { “files”: [ { “recType”: 2, “startTime”: 1691627391000, “endTime”: 1691627443000, “deviceSerial”: “G12262381”, “channelNo”: 1, “localType”: “ALARM”, “channelType”: “D”, “id”: null, “fileId”: null, “ownerId”: null, “fileType”: 0, “fileName”: null, “cloudType”: 0, “fileIndex”: null, “fileSize”: 0, “locked”: 0, “createTime”: 0, “crypt”: 0, “keyChecksum”: null, “videoLong”: 0, “coverPic”: null, “downloadPath”: null, “type”: 1, “iStorageVersion”: null, “videoType”: null }, { “recType”: 2, “startTime”: 1691627491000, “endTime”: 1691627537000, “deviceSerial”: “G12262381”, “channelNo”: 1, “localType”: “ALARM”, “channelType”: “D”, “id”: null, “fileId”: null, “ownerId”: null, “fileType”: 0, “fileName”: null, “cloudType”: 0, “fileIndex”: null, “fileSize”: 0, “locked”: 0, “createTime”: 0, “crypt”: 0, “keyChecksum”: null, “videoLong”: 0, “coverPic”: null, “downloadPath”: null, “type”: 1, “iStorageVersion”: null, “videoType”: null }, { “recType”: 2, “startTime”: 1691627537000, “endTime”: 1691627586000, “deviceSerial”: “G12262381”, “channelNo”: 1, “localType”: “ALARM”, “channelType”: “D”, “id”: null, “fileId”: null, “ownerId”: null, “fileType”: 0, “fileName”: null, “cloudType”: 0, “fileIndex”: null, “fileSize”: 0, “locked”: 0, “createTime”: 0, “crypt”: 0, “keyChecksum”: null, “videoLong”: 0, “coverPic”: null, “downloadPath”: null, “type”: 1, “iStorageVersion”: null, “videoType”: null } ], “isAll”: false, “nextFileTime”: 1691627586000 } } OPEN API/云直播/录像文件查询/设备本地录像查询/查询设备本地录像 查询设备本地录像 接口URL GET https://open.ys7.com/api/v3/device/local/video/unify/query 请求 Header 名称 类型 必填 描述 示例指及参考API accessToken string Y 萤石开放平台令牌,支持托管 、子账号、设备小权限token,权限为Replay deviceSerial string Y 设备序列号 localIndex string N 通道号 query 名称 类型 必填 描述 示例指及参考API recordType int N 1:定时录像 2:事件录像 3:智能-车 4:智能-人形 5:自动浓缩录像,不填默认查询所有类型 startTime string Y 开始时间,时间格式为:1731988238,开始结束时间必须在同一天,开始时间不能大于结束时间 endTime string Y 结束时间,时间格式为:1732007943,开始结束时间必须在同一天,开始时间不能大于结束时间 isQueryByNvr int N 是否反查NVR录像:0-不反查(默认),1-反查NVR location int N 录像检索位置:1-本地录像检索(默认),2-CVR中心录像检索 pageSize int N 分页的页面大小,默认50,最大200 响应 返数据 名称 类型 描述 示例 meta object meta -code int code -message string message -moreInfo object moreInfo data object data -records arrayrecords –startTime int startTime –endTime int endTime –type string type –size string size -fromNvr boolean fromNvr -deviceSerial string deviceSerial -localIndex string localIndex -hasMore boolean hasMore -nextFileTime int nextFileTime 返示例 { “meta”: { “code”: 200, “message”: “操作成功”, “moreInfo”: null }, “data”: { “records”: [ { “startTime”: 1731945592, “endTime”: 1731949200, “type”: “ALARM”, //录像类型 “size”: “” //录像文件大小,单位:字节 } ], “fromNvr”: true, //该录像文件是否来自关联的nvr “deviceSerial”: “J79401957”, // fromNvr为true,则返关联NVR设备序列号,否则返入参填的设备序列号。 “localIndex”: “1”, // fromNvr为true,则返关联NVR设备通道号,否则返入参填的设备通道号。 “hasMore”: true, //是否存在更多录像文件 “nextFileTime”: 1732007943// hasMore为true时,该参数值为下一个录像文件的开始时间。如需分页查询,该参数值可作为下一页录像文件查询的开始时间。 } } SDK及示例/Android SDK/Android 放/放 对摄像机存储于SD卡、云端的录像进行取流,查看当前摄像机的历史放画面。 第一步创建播放器 可调用EZOpenSDK中的 createPlayer 方法创建播放器。 第二步配置播放器 播放器创建完成后需要进行设置代理,设置播放视图,验证码设置等配置。 第三步开始播放 调用startPlayback(EZCloudRecordFile cloudFile) 或 startPlayback(EZDeviceRecordFile deviceFile) 开始放 第四步结束播放 调用stopPlayback结束放 5.第五步释放播放器 调用release释放播放器 完整示例代码如下: public class EZPlayBackListActivity extends RootActivity implements TextureView.SurfaceTextureListener, Handler.Callback, … { private EZPlayer mPlaybackPlayer = null; /** 点击录像片段后调用 */ private void initEZPlayer() { if (mPlaybackPlayer != null) { // do something // 停止播放 mPlaybackPlayer.stopPlayback(); } else { // 创建播放器,也可以直接使用EZPlayer类中的方法创建 mPlaybackPlayer = getOpenSDK().createPlayer(mCameraInfo.getDeviceSerial(), mCameraInfo.getCameraNo()); // 设置Handler, 该handler将被用于从播放器向handler传递消息 mPlaybackPlayer.setHandler(playBackHandler); // 设置播放器的显示Surface mPlaybackPlayer.setSurfaceEx(mTextureView.getSurfaceTexture()); // 可选,设备开启了视频/图片加密功能后需设置,可根据EZDeviceInfo的isEncrypt属性判断 mPlaybackPlayer.setPlayVerifyCode(verifyCode); // 放云端存储的视频,cloudFile由EZOpenSDK.searchRecordFileFromCloud接口获取 mPlaybackPlayer.startPlayback(cloudFile); // 或者 // 放设备上存储的视频,deviceFile由EZOpenSDK.searchRecordFileFromDevice接口获取 mPlaybackPlayer.startPlayback(deviceFile); } } @Override protected void onStop() { super.onStop(); if (mPlaybackPlayer != null) { // 页面退出或用户主动停止播放时调用stopPlayback结束放 mPlaybackPlayer.stopPlayback(); } // do something } @Override protected void onDestroy() { super.onDestroy(); if (mPlaybackPlayer != null) { // 调用release释放播放器 mPlaybackPlayer.release(); } // do something } @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { // 画面显示第一帧 case EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_SUCCUSS:// 录像放成功 // do something break; case EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_START:// 播放开始|seek成功 // do something break; case EZPlaybackConstants.MSG_REMOTEPLAYBACK_STOP_SUCCESS:// 录像放停止 // do something break; case EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_FINISH:// 录像放完成 // do something break; case EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_FAIL:// 录像放失败 ErrorInfo errorInfo = (ErrorInfo) msg.obj; int errorCode = errorInfo.errorCode; // 如果是需要验证码或者是验证码错误 if (errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_NEED || errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_ERROR) { // do something } else { // do something } break; default: break; } } } 说明 放需先获取到视频信息,searchRecordFileFromCloud方法和searchRecordFileFromDevice方法分别是获取云端视频列表和设备存储视频列表的两个方法。 开始播放之后在handleMessage调中会收到通知消息,播放成功消息为EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_SUCCUSS, 播放失败消息为EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_FAIL,如果是错误码ErrorCode.ERROR_INNER_VERIFYCODE_NEED = 400035(需要设备验证码)或者ErrorCode.ERROR_INNER_VERIFYCODE_ERROR = 400036(设备验证码不匹配),需要开发者自己处理让用户输入验证密码,然后调用EZPlayer.setPlayVerifyCode设置密码,重新启动播放。 注意:实际录像放结束时间可能与录像片段的结束时间有偏差,如果时间点相近则认为放结束,此为正常现象。严格依据上述开发文档检查并解决从MainActivity.java点击id.huifang后进入FanHui.java整个程序卡死无任何响应。MainActivity.java:package com.videogo.ui.login; import android.content.res.Configuration; import android.content.pm.ActivityInfo; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import com.videogo.exception.BaseException; import com.videogo.exception.ErrorCode; import com.videogo.openapi.EZConstants; import com.videogo.openapi.EZOpenSDK; import com.videogo.openapi.EZPlayer; import ezviz.ezopensdk.R; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.LinearLayout; import android.widget.ImageButton; import android.widget.Toast; import android.view.MotionEvent; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URLEncoder; import org.json.JSONObject; import android.content.Intent; import android.widget.Button; // 新增导入 import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback, Handler.Callback { private static final String TAG = “EZPreview”; private static final int MSG_VIDEO_SIZE_CHANGED = 1; private static final int MSG_REALPLAY_PLAY_SUCCESS = 2001; private static final int MSG_REALPLAY_PLAY_FAIL = 2002; private static final int MSG_SHOW_STREAM_TYPE = 3001; // 新增消息类型 private static final int DIRECTION_UP = 0; private static final int DIRECTION_DOWN = 1; private static final int DIRECTION_LEFT = 2; private static final int DIRECTION_RIGHT = 3; private static final int DIRECTION_ZOOM_IN = 8; // 物理放大 private static final int DIRECTION_ZOOM_OUT = 9; // 物理缩小 private static final int DIRECTION_FOCUS_NEAR = 10; // 调整近焦距 private static final int DIRECTION_FOCUS_FAR = 11; // 调整远焦距 private static final int SPEED_MEDIUM = 1; // 速度适中 private static final String PTZ_START_URL = “https://open.ys7.com/api/lapp/device/ptz/start”; private static final String PTZ_STOP_URL = “https://open.ys7.com/api/lapp/device/ptz/stop”; private boolean mIsSupportPTZ = false; // 设备是否支持云台 private ExecutorService mExecutorService; // 线程池 private volatile boolean isPTZActive = false; // 标记是否有云台操作正在进行 private volatile int activeDirection = -1; // 当前活动的方向 private boolean mHasShownConnectionSuccess = false; // 接收的参数键 private static final String KEY_APPKEY = “appkey”; private static final String KEY_SERIAL = “serial”; private static final String KEY_VERIFYCODE = “VerifyCode”; private static final String KEY_ACCESSTOKEN = “accessToken”; private static final String KEY_CAMERANO = “cameraNo”; private boolean mIsPlaying = false; private EZPlayer mEZPlayer; private SurfaceView mSurfaceView; private SurfaceHolder mSurfaceHolder; private ProgressBar mLiveProgressBar; private RelativeLayout mRlControl; private LinearLayout mLlHc; private ImageButton mIbRotate2; private RelativeLayout mRaTitle; // 从Intent中获取的参数 private String mAppKey; private String mDeviceSerial; private String mVerifyCode; private String mAccessToken; private int mCameraNo = 0; // 默认通道号0 private Handler mHandler; private boolean mP2PEnabled = true; // 新增:云台控制重试机制相关变量 private static final int MAX_PTZ_RETRIES = 2; private Map<Integer, Integer> ptzRetryCountMap = new ConcurrentHashMap<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activitymain); mHandler = new Handler(this); mRaTitle = findViewById(R.id.ra_title); // 1. 从Intent中获取参数 extractParametersFromIntent(); // 2. 初始化UI initUI(); initOrientationSensitiveViews(); View fanHui = findViewById(R.id.back); fanHui.setOnClickListener(v -> finish()); Button huifangBtn = findViewById(R.id.huifang); huifangBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 创建Intent跳转到FanHui活动 Intent intent = new Intent(MainActivity.this, FanHui.class); // 传递必要参数(可选) intent.putExtra("deviceSerial", mDeviceSerial); intent.putExtra("cameraNo", mCameraNo); intent.putExtra("accessToken", mAccessToken); intent.putExtra("appkey", mAppKey); intent.putExtra("verifyCode", mVerifyCode); startActivity(intent); } }); // 3. 初始化SDK并创建播放器 initSDKAndCreatePlayer(); } private void initOrientationSensitiveViews() { mLiveProgressBar = findViewById(R.id.liveProgressBar); mRlControl = findViewById(R.id.rl_control); mLlHc = findViewById(R.id.ll_hc); mIbRotate2 = findViewById(R.id.ib_rotate2); // 初始状态显示加载 if (mLiveProgressBar != null) { mLiveProgressBar.setVisibility(View.VISIBLE); } // 初始根据方向更新UI updateLayoutByOrientation(); // 创建线程池 mExecutorService = Executors.newFixedThreadPool(2); // 4. 初始化云台控制按钮 initPTZButtons(); } private void initPTZButtons() { // 竖屏布局的按钮 setupPTZButton(R.id.ptz_left_btn, DIRECTION_LEFT); setupPTZButton(R.id.ptz_right_btn, DIRECTION_RIGHT); setupPTZButton(R.id.ptz_top_btn, DIRECTION_UP); setupPTZButton(R.id.ptz_bottom_btn, DIRECTION_DOWN); setupPTZButton(R.id.zoom_add, DIRECTION_ZOOM_IN); // 物理放大 setupPTZButton(R.id.zoom_reduce, DIRECTION_ZOOM_OUT); // 物理缩小 setupPTZButton(R.id.focus_add, DIRECTION_FOCUS_NEAR); // 近焦距 setupPTZButton(R.id.foucus_reduce, DIRECTION_FOCUS_FAR); // 远焦距 setupUnsupportedButton(R.id.guangquan_add, "光圈+"); setupUnsupportedButton(R.id.guangquan_reduce, "光圈-"); // 横屏布局的按钮 setupPTZButton(R.id.ptz_left_btn2, DIRECTION_LEFT); setupPTZButton(R.id.ptz_right_btn2, DIRECTION_RIGHT); setupPTZButton(R.id.ptz_top_btn2, DIRECTION_UP); setupPTZButton(R.id.ptz_bottom_btn2, DIRECTION_DOWN); setupPTZButton(R.id.zoom_add2, DIRECTION_ZOOM_IN); setupPTZButton(R.id.zoom_reduce2, DIRECTION_ZOOM_OUT); setupPTZButton(R.id.focus_add2, DIRECTION_FOCUS_NEAR); // 近焦距 setupPTZButton(R.id.foucus_reduce2, DIRECTION_FOCUS_FAR); // 远焦距 } private void setupUnsupportedButton(int buttonId, String buttonName) { ImageButton button = findViewById(buttonId); if (button != null) { button.setOnClickListener(v -> { Toast.makeText(MainActivity.this, "SDK不支持此功能", Toast.LENGTH_SHORT).show(); }); } } private void setupPTZButton(int buttonId, final int direction) { ImageButton button = findViewById(buttonId); if (button != null) { button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 按下时开始云台动作 controlPTZ(direction, SPEED_MEDIUM, true); return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // 如果这是当前活动的方向,则停止 if (isPTZActive && activeDirection == direction) { controlPTZ(direction, SPEED_MEDIUM, false); } return true; } return false; } }); } } // 重构后的云台控制方法 private void controlPTZ(final int direction, final int speed, final boolean isStart) { // 检查是否允许开始新操作 if (isStart) { if (isPTZActive) { // 如果有活动操作,先停止当前操作 controlPTZ(activeDirection, speed, false); // 延迟100ms后再开始新操作 new Handler().postDelayed(() -> controlPTZ(direction, speed, true), 100); return; } } // 重试次数检查 Integer retryCount = ptzRetryCountMap.getOrDefault(direction, 0); if (retryCount >= MAX_PTZ_RETRIES) { Log.w(TAG, "达到最大重试次数,放弃操作 direction=" + direction); ptzRetryCountMap.remove(direction); resetPTZState(); return; } mExecutorService.execute(() -> { try { String urlStr = isStart ? PTZ_START_URL : PTZ_STOP_URL; URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setDoOutput(true); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); // 构建POST数据 StringBuilder postData = new StringBuilder(); postData.append("accessToken=").append(URLEncoder.encode(mAccessToken, "UTF-8")); postData.append("&deviceSerial=").append(URLEncoder.encode(mDeviceSerial, "UTF-8")); postData.append("&channelNo=").append(mCameraNo); if (isStart) { postData.append("&direction=").append(direction); postData.append("&speed=").append(speed); } else { // 停止时带上方向参数(建议) postData.append("&direction=").append(direction); } // 发送请求 OutputStream os = conn.getOutputStream(); os.write(postData.toString().getBytes("UTF-8")); os.flush(); os.close(); // 处理响应 int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { // 读取响应内容 InputStream is = conn.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); // 解析JSON响应 JSONObject json = new JSONObject(response.toString()); String code = json.optString("code", "0"); String msg = json.optString("msg", "未知错误"); if ("200".equals(code)) { Log.d(TAG, "PTZ控制成功: " + (isStart ? "开始" : "停止") + " | 方向: " + direction); // 更新状态 if (isStart) { isPTZActive = true; activeDirection = direction; } else { isPTZActive = false; activeDirection = -1; } // 成功时重置重试计数器 ptzRetryCountMap.remove(direction); } else { // 增加重试计数 ptzRetryCountMap.put(direction, retryCount + 1); if (isStart) { Log.w(TAG, "PTZ控制失败,尝试重试 direction=" + direction + ", retry=" + (retryCount + 1)); controlPTZ(direction, speed, true); } else { // 停止命令失败需要特殊处理 handleStopFailure(direction); } handlePTZError(code, msg, isStart); } } else { // 增加重试计数 ptzRetryCountMap.put(direction, retryCount + 1); if (isStart) { Log.w(TAG, "PTZ控制失败,尝试重试 direction=" + direction + ", retry=" + (retryCount + 1)); controlPTZ(direction, speed, true); } else { // 停止命令失败需要特殊处理 handleStopFailure(direction); } Log.e(TAG, "HTTP错误: " + responseCode); runOnUiThread(() -> Toast.makeText(MainActivity.this, "网络请求失败: " + responseCode, Toast.LENGTH_SHORT).show()); } conn.disconnect(); } catch (Exception e) { // 增加重试计数 ptzRetryCountMap.put(direction, retryCount + 1); if (isStart && retryCount < MAX_PTZ_RETRIES) { Log.w(TAG, "PTZ控制异常,尝试重试", e); controlPTZ(direction, speed, true); } else { handleStopFailure(direction); } Log.e(TAG, "PTZ控制异常", e); runOnUiThread(() -> Toast.makeText(MainActivity.this, "云台控制出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } }); } // 新增:处理停止失败的情况 private void handleStopFailure(int direction) { runOnUiThread(() -> { // 紧急停止:发送不带方向参数的停止命令 emergencyStopPTZ(); Toast.makeText(MainActivity.this, "云台停止失败,已执行紧急停止", Toast.LENGTH_SHORT).show(); }); // 重置状态 resetPTZState(); } // 新增:紧急停止方法(不带方向参数) private void emergencyStopPTZ() { mExecutorService.execute(() -> { try { URL url = new URL(PTZ_STOP_URL); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setDoOutput(true); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); // 构建不带方向参数的POST数据 StringBuilder postData = new StringBuilder(); postData.append("accessToken=").append(URLEncoder.encode(mAccessToken, "UTF-8")); postData.append("&deviceSerial=").append(URLEncoder.encode(mDeviceSerial, "UTF-8")); postData.append("&channelNo=").append(mCameraNo); // 发送请求 OutputStream os = conn.getOutputStream(); os.write(postData.toString().getBytes("UTF-8")); os.flush(); os.close(); // 处理响应 int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { // 读取响应内容 InputStream is = conn.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); // 解析JSON响应 JSONObject json = new JSONObject(response.toString()); String code = json.optString("code", "0"); if ("200".equals(code)) { Log.d(TAG, "紧急停止成功"); } else { Log.w(TAG, "紧急停止失败: " + json.optString("msg", "未知错误")); } } else { Log.e(TAG, "紧急停止HTTP错误: " + responseCode); } conn.disconnect(); } catch (Exception e) { Log.e(TAG, "紧急停止异常", e); } }); } // 新增:重置云台状态 private void resetPTZState() { isPTZActive = false; activeDirection = -1; ptzRetryCountMap.clear(); } private void handlePTZError(String code, String msg, boolean isStart) { String errorMessage = msg; // 根据文档映射错误信息 switch (code) { case "10001": errorMessage = "参数错误"; break; case "10002": errorMessage = "accessToken异常或过期"; break; case "20002": errorMessage = "设备不存在"; break; case "20007": errorMessage = "设备不在线"; break; case "60000": errorMessage = "设备不支持云台控制"; mIsSupportPTZ = false; // 更新设备支持状态 break; case "60001": errorMessage = "用户无云台控制权限"; break; case "60020": errorMessage = "不支持该命令"; break; // 添加其他错误码处理... } final String finalMsg = errorMessage; runOnUiThread(() -> { Toast.makeText(MainActivity.this, (isStart ? "开始" : "停止") + "云台控制失败: " + finalMsg, Toast.LENGTH_LONG).show(); // 对于特定错误,重置云台状态 if ("10002".equals(code) || "60000".equals(code)) { isPTZActive = false; activeDirection = -1; } }); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // 方向变化时重新初始化视图 initOrientationSensitiveViews(); updateLayoutByOrientation(); } @Override protected void onResume() { super.onResume(); if (mLiveProgressBar != null) { if (mIsPlaying) { mLiveProgressBar.setVisibility(View.GONE); } else { mLiveProgressBar.setVisibility(View.VISIBLE); } } } private void updateLayoutByOrientation() { int orientation = getResources().getConfiguration().orientation; if (orientation == Configuration.ORIENTATION_PORTRAIT) { // 竖屏模式 mRaTitle.setVisibility(View.VISIBLE); mRlControl.setVisibility(View.VISIBLE); mLlHc.setVisibility(View.GONE); mIbRotate2.setVisibility(View.GONE); } else { // 横屏模式 mRaTitle.setVisibility(View.GONE); mRlControl.setVisibility(View.GONE); mLlHc.setVisibility(View.VISIBLE); mIbRotate2.setVisibility(View.VISIBLE); } } /** * 从Intent中提取传递的参数 */ private void extractParametersFromIntent() { Bundle extras = getIntent().getExtras(); if (extras != null) { mAppKey = extras.getString(KEY_APPKEY, ""); mDeviceSerial = extras.getString(KEY_SERIAL, ""); mVerifyCode = extras.getString(KEY_VERIFYCODE, ""); mAccessToken = extras.getString(KEY_ACCESSTOKEN, ""); mCameraNo = extras.getInt(KEY_CAMERANO, 0); Log.d(TAG, "Received parameters:"); Log.d(TAG, "AppKey: " + mAppKey); Log.d(TAG, "DeviceSerial: " + mDeviceSerial); Log.d(TAG, "VerifyCode: " + mVerifyCode); Log.d(TAG, "AccessToken: " + mAccessToken); Log.d(TAG, "CameraNo: " + mCameraNo); } else { Log.e(TAG, "No parameters received from intent"); // 如果没有参数,可以显示错误信息并退出 finish(); } } /** * 初始化UI组件 */ private void initUI() { mSurfaceView = findViewById(R.id.realplay_sv); if (mSurfaceView != null) { mSurfaceHolder = mSurfaceView.getHolder(); mSurfaceHolder.addCallback(this); } else { Log.e(TAG, "SurfaceView not found with ID realplay_sv"); } } /** * 初始化SDK并创建播放器 */ private void initSDKAndCreatePlayer() { try { // 1. 初始化SDK EZOpenSDK.initLib(getApplication(), mAppKey); EZOpenSDK.getInstance().setAccessToken(mAccessToken); // +++ 开启P2P取流方式 +++ EZOpenSDK.enableP2P(true); // 开启P2P取流 Log.d(TAG, "P极取流已启用"); mP2PEnabled = true; // 2. 创建播放器 createPlayer(); } catch (Exception e) { Log.e(TAG, "SDK初始化失败", e); Toast.makeText(this, "SDK初始化失败", Toast.LENGTH_SHORT).show(); } } /** * 创建播放器并开始播放 */ private void createPlayer() { try { // 1. 创建播放器实例 mEZPlayer = EZOpenSDK.getInstance().createPlayer(mDeviceSerial, mCameraNo); // 2. 配置播放器 mEZPlayer.setHandler(mHandler); if (mSurfaceHolder != null) { mEZPlayer.setSurfaceHold(mSurfaceHolder); } if (mVerifyCode != null && !mVerifyCode.isEmpty()) { mEZPlayer.setPlayVerifyCode(mVerifyCode); } Toast.makeText(MainActivity.this, "连接成功", Toast.LENGTH_SHORT).show(); // 3. 开始播放 mEZPlayer.startRealPlay(); mIsPlaying = true; // 标记为正在播放 } catch (Exception e) { Log.e(TAG, "Player creation failed", e); mIsPlaying = false; // 标记为未播放 } } // 处理屏幕旋转按钮点击 public void changeScreen(View view) { Log.d(TAG, "Change screen orientation requested"); int currentOrientation = getResources().getConfiguration().orientation; if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } // 更新UI布局 updateLayoutByOrientation(); } // Surface调接口实现 @Override public void surfaceCreated(@NonNull SurfaceHolder holder) { if (mEZPlayer != null) { mEZPlayer.setSurfaceHold(holder); } } @Override public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {} @Override public void surfaceDestroyed(@NonNull SurfaceHolder holder) { if (mEZPlayer != null) { mEZPlayer.setSurfaceHold(null); } } @Override protected void onStop() { super.onStop(); // 确保云台停止 if (isPTZActive && activeDirection != -1) { Log.d(TAG, "Activity停止,强制停止云台"); controlPTZ(activeDirection, SPEED_MEDIUM, false); } if (mEZPlayer != null) { mEZPlayer.stopRealPlay(); mIsPlaying = false; // 标记为已停止 } } // Handler处理播放状态 @Override public boolean handleMessage(@NonNull Message msg) { Log.d(TAG, "handleMessage: " + msg.what); switch (msg.what) { case MSG_VIDEO_SIZE_CHANGED: break; case MSG_REALPLAY_PLAY_SUCCESS: Log.i(TAG, "播放成功"); mIsPlaying = true; // 获取并显示取流方式 int streamType = mEZPlayer.getStreamFetchType(); String streamTypeName = getStreamTypeName(streamType); Log.d(TAG, "当前取流方式: " + streamTypeName); // 发送消息显示取流方式 Message showMsg = new Message(); showMsg.what = MSG_SHOW_STREAM_TYPE; showMsg.obj = streamTypeName; mHandler.sendMessage(showMsg); runOnUiThread(() -> { if (mLiveProgressBar != null) { mLiveProgressBar.setVisibility(View.GONE); } if (!mHasShownConnectionSuccess) { mHasShownConnectionSuccess = true; } }); // 在播放成功后检查设备是否支持云台 checkDevicePTZSupport(); break; case MSG_REALPLAY_PLAY_FAIL: Log.e(TAG, "播放失败"); mIsPlaying = false; runOnUiThread(() -> { if (mLiveProgressBar != null) { mLiveProgressBar.setVisibility(View.VISIBLE); } }); BaseException error = (BaseException) msg.obj; int errorCode = error.getErrorCode(); if (errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_NEED || errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_ERROR) { mVerifyCode = "123456"; if (mEZPlayer != null) { mEZPlayer.setPlayVerifyCode(mVerifyCode); mEZPlayer.startRealPlay(); } } else { Log.e(TAG, "播放失败,错误码: " + errorCode); } break; case MSG_SHOW_STREAM_TYPE: String type = (String) msg.obj; Toast.makeText(MainActivity.this, "取流方式: " + type + (mP2PEnabled ? " (P2P已启用)" : ""), Toast.LENGTH_LONG).show(); break; } return true; } private void checkDevicePTZSupport() { new Thread(() -> { try { // 这里使用SDK方法检查设备能力 // 如果SDK没有提供方法,可以使用HTTP API查询设备能力 // 简化处理:假设所有设备都支持云台 mIsSupportPTZ = true; // 实际项目中应该查询设备能力 // mIsSupportPTZ = queryDevicePTZCapability(); } catch (Exception e) { Log.e(TAG, "Failed to check PTZ support", e); } }).start(); } @Override protected void onDestroy() { super.onDestroy(); if (mExecutorService != null) { mExecutorService.shutdown(); } // 移除Handler调避免内存泄漏 if (mHandler != null) { mHandler.removeCallbacksAndMessages(null); } if (mEZPlayer != null) { mEZPlayer.release(); mEZPlayer = null; } } private String getStreamTypeName(int type) { switch (type) { case 0: return "流媒体"; case 1: return "P2P"; case 2: return "内网直连"; case 4: return "云存储"; default: return "未知(" + type + ")"; } } } FanHui.java:package com.videogo.ui.login; import android.app.AlertDialog; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.DatePicker; import android.widget.ImageButton; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.videogo.exception.BaseException; import com.videogo.exception.ErrorCode; import com.videogo.openapi.EZConstants; import com.videogo.openapi.EZPlayer; import androidx.appcompat.app.AppCompatActivity; import com.videogo.openapi.EZOpenSDK; import ezviz.ezopensdk.R; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class FanHui extends AppCompatActivity implements SurfaceHolder.Callback { private static final String TAG = "EZPlayback"; private String mAppKey; private String mDeviceSerial; private String mVerifyCode; private String mAccessToken; private int mCameraNo; private TextView mDateTextView; private int mSelectedYear, mSelectedMonth, mSelectedDay; private static final String KEY_APPKEY = "appkey"; private static final String KEY_SERIAL = "serial"; private static final String KEY_VERIFYCODE = "VerifyCode"; private static final String KEY_ACCESSTOKEN = "accessToken"; private static final String KEY_CAMERANO = "cameraNo"; // 放录像相关 private static final String VIDEO_BY_TIME_URL = "https://open.ys7.com/api/lapp/video/by/time"; private ExecutorService mExecutorService; private ListView mListView; private PlaybackAdapter mAdapter; private List<VideoInfo> mVideoList = new ArrayList<>(); // 播放器相关 private EZPlayer mEZPlayer; private SurfaceView mPlaybackSurfaceView; private SurfaceHolder mSurfaceHolder; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.ez_playback_list_page); // 创建线程池 mExecutorService = Executors.newFixedThreadPool(2); extractParametersFromIntent(); final Calendar calendar = Calendar.getInstance(); mSelectedYear = calendar.get(Calendar.YEAR); mSelectedMonth = calendar.get(Calendar.MONTH); mSelectedDay = calendar.get(Calendar.DAY_OF_MONTH); // 初始化视图 initViews(); // 设置日期显示模块 setupDatePicker(); // 默认加载当天的录像 loadVideosForSelectedDate(); // 初始化SDK initSDK(); } private void initSDK() { try { // 初始化萤石云SDK EZOpenSDK.initLib(getApplication(), mAppKey); EZOpenSDK.getInstance().setAccessToken(mAccessToken); } catch (Exception e) { Log.e(TAG, "SDK初始化失败", e); Toast.makeText(this, "SDK初始化失败", Toast.LENGTH_SHORT).show(); } } private void initViews() { // 查找ListView mListView = findViewById(R.id.listView); if (mListView == null) { Log.e(TAG, "ListView not found with ID listView"); return; } // 初始化适配器 mAdapter = new PlaybackAdapter(); mListView.setAdapter(mAdapter); // 设置点击事件 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { VideoInfo video = mVideoList.get(position); playVideo(video); } }); // 获取播放器视图 - 使用SurfaceView mPlaybackSurfaceView = findViewById(R.id.remote_playback_wnd_sv); if (mPlaybackSurfaceView != null) { // 设置SurfaceHolder调 mSurfaceHolder = mPlaybackSurfaceView.getHolder(); mSurfaceHolder.addCallback(this); } else { Log.e(TAG, "SurfaceView not found with ID remote_playback_wnd_sv"); } } // SurfaceHolder调方法 @Override public void surfaceCreated(SurfaceHolder holder) { Log.d(TAG, "Surface created"); // 当Surface创建时,如果有播放器实例,设置SurfaceHolder if (mEZPlayer != null) { mEZPlayer.setSurfaceHold(holder); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.d(TAG, "Surface changed: " + width + "x" + height); } @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.d(TAG, "Surface destroyed"); // 当Surface销毁时,释放播放器资源 stopPlayback(); } private void setupDatePicker() { mDateTextView = findViewById(R.id.date_text); ImageButton datePickerButton = findViewById(R.id.date_picker_button); updateDateDisplay(); datePickerButton.setOnClickListener(v -> showDatePickerDialog()); } private void updateDateDisplay() { String formattedDate = String.format(Locale.getDefault(), "%d年%02d月%02d日", mSelectedYear, mSelectedMonth + 1, // 月份需要+1 mSelectedDay); mDateTextView.setText(formattedDate); } private void showDatePickerDialog() { final AlertDialog dlg = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog).create(); dlg.show(); Window window = dlg.getWindow(); window.setContentView(R.layout.datepicker_layout); // 设置对话框宽度 WindowManager.LayoutParams lp = window.getAttributes(); lp.width = WindowManager.LayoutParams.MATCH_PARENT; window.setAttributes(lp); // 获取并初始化 DatePicker DatePicker dpPicker = window.findViewById(R.id.dpPicker); // 隐藏不需要的视图 ViewGroup rootView = (ViewGroup) dpPicker.getChildAt(0); if (rootView != null) { ViewGroup childView = (ViewGroup) rootView.getChildAt(0); if (childView != null) { childView.getChildAt(2).setVisibility(View.VISIBLE); // 确保月选择器可见 childView.getChildAt(1).setVisibility(View.VISIBLE); } } dpPicker.init(mSelectedYear, mSelectedMonth, mSelectedDay, null); // 设置按钮事件 RelativeLayout yesButton = window.findViewById(R.id.YES); RelativeLayout noButton = window.findViewById(R.id.NO); yesButton.setOnClickListener(v -> { mSelectedYear = dpPicker.getYear(); mSelectedMonth = dpPicker.getMonth(); mSelectedDay = dpPicker.getDayOfMonth(); updateDateDisplay(); dlg.dismiss(); // 加载新选择的日期的录像 loadVideosForSelectedDate(); }); noButton.setOnClickListener(v -> dlg.dismiss()); } private void extractParametersFromIntent() { Bundle extras = getIntent().getExtras(); if (extras != null) { mAppKey = extras.getString(KEY_APPKEY, ""); mDeviceSerial = extras.getString(KEY_SERIAL, ""); mVerifyCode = extras.getString(KEY_VERIFYCODE, ""); mAccessToken = extras.getString(KEY_ACCESSTOKEN, ""); mCameraNo = extras.getInt(KEY_CAMERANO, 0); Log.d(TAG, "Received parameters:"); Log.d(TAG, "AppKey: " + mAppKey); Log.d(TAG, "DeviceSerial: " + mDeviceSerial); Log.d(TAG, "VerifyCode: " + mVerifyCode); Log.d(TAG, "AccessToken: " + mAccessToken); Log.d(TAG, "CameraNo: " + mCameraNo); } else { Log.e(TAG, "No parameters received from intent"); } } private void loadVideosForSelectedDate() { // 计算开始和结束时间戳 Calendar cal = Calendar.getInstance(); cal.set(mSelectedYear, mSelectedMonth, mSelectedDay, 0, 0, 0); long startTime = cal.getTimeInMillis(); cal.set(mSelectedYear, mSelectedMonth, mSelectedDay, 23, 59, 59); long endTime = cal.getTimeInMillis(); // 发起网络请求获取录像 fetchVideosByTime(startTime, endTime); } private void fetchVideosByTime(long startTime, long endTime) { mExecutorService.execute(() -> { try { URL url = new URL(VIDEO_BY_TIME_URL); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setDoOutput(true); conn.setConnectTimeout(10000); conn.setReadTimeout(10000); // 构建POST数据(添加分页参数) StringBuilder postData = new StringBuilder(); postData.append("accessToken=").append(URLEncoder.encode(mAccessToken, "UTF-8")); postData.append("&deviceSerial=").append(URLEncoder.encode(mDeviceSerial, "UTF-8")); postData.append("&channelNo=").append(mCameraNo); postData.append("&startTime=").append(startTime); postData.append("&endTime=").append(endTime); postData.append("&recType=").append(0); // 系统自动选择 postData.append("&version=2.0"); // 添加分页版本 postData.append("&pageSize=100"); // 添加分页大小 // 发送请求 OutputStream os = conn.getOutputStream(); os.write(postData.toString().getBytes("UTF-8")); os.flush(); os.close(); // 处理响应 int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { // 读取响应内容 InputStream is = conn.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); // 解析JSON响应 JSONObject json = new JSONObject(response.toString()); String code = json.optString("code", "0"); if ("200".equals(code)) { JSONArray data = json.getJSONArray("data"); List<VideoInfo> videos = parseVideoData(data); runOnUiThread(() -> { mVideoList.clear(); mVideoList.addAll(videos); mAdapter.notifyDataSetChanged(); }); } else { String msg = json.optString("msg", "未知错误"); Log.e(TAG, "获取录像失败: " + msg); runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像失败: " + msg, Toast.LENGTH_SHORT).show()); } } else { Log.e(TAG, "HTTP错误: " + responseCode); runOnUiThread(() -> Toast.makeText(FanHui.this, "网络请求失败: " + responseCode, Toast.LENGTH_SHORT).show()); } conn.disconnect(); } catch (Exception e) { Log.e(TAG, "获取录像异常", e); runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } }); } private List<VideoInfo> parseVideoData(JSONArray data) throws JSONException { List<VideoInfo> videos = new ArrayList<>(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); for (int i = 0; i < data.length(); i++) { JSONObject videoObj = data.getJSONObject(i); VideoInfo video = new VideoInfo(); video.id = videoObj.optString("id"); video.startTime = videoObj.optLong("startTime"); video.endTime = videoObj.optLong("endTime"); video.recType = videoObj.optInt("recType"); // 格式化时间显示 Date startDate = new Date(video.startTime); Date endDate = new Date(video.endTime); video.timeRange = sdf.format(startDate) + " - " + sdf.format(endDate); videos.add(video); } return videos; } private void playVideo(VideoInfo video) { // 停止当前播放 stopPlayback(); try { // 创建播放器实例 mEZPlayer = EZOpenSDK.getInstance().createPlayer(mDeviceSerial, mCameraNo); // 配置播放器 - 设置处理器 mEZPlayer.setHandler(mHandler); // 设置验证码 mEZPlayer.setPlayVerifyCode(mVerifyCode); // 关联播放视图 - 使用SurfaceHolder(修复方法名) if (mSurfaceHolder != null) { mEZPlayer.setSurfaceHold(mSurfaceHolder); } else { Log.e(TAG, "无法关联播放视图"); Toast.makeText(this, "播放视图未初始化", Toast.LENGTH_SHORT).show(); } // 创建Calendar对象作为参数 Calendar startCal = Calendar.getInstance(); startCal.setTimeInMillis(video.startTime); Calendar endCal = Calendar.getInstance(); endCal.setTimeInMillis(video.endTime); // 开始放 mEZPlayer.startPlayback(startCal, endCal); Toast.makeText(this, "开始播放录像: " + video.timeRange, Toast.LENGTH_SHORT).show(); } catch (Exception e) { Log.e(TAG, "播放录像失败", e); Toast.makeText(this, "播放录像失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } private void stopPlayback() { if (mEZPlayer != null) { mEZPlayer.stopPlayback(); mEZPlayer.release(); mEZPlayer = null; } } // 播放器处理器 private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case EZConstants.EZRealPlayConstants.MSG_REALPLAY_PLAY_SUCCESS: Log.i(TAG, "放播放成功"); break; case EZConstants.EZRealPlayConstants.MSG_REALPLAY_PLAY_FAIL: Log.e(TAG, "放播放失败"); BaseException error = (BaseException) msg.obj; int errorCode = error.getErrorCode(); String errorMsg = "播放失败: " + errorCode; // 根据错误码提供更具体的错误信息 if (errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_NEED) { errorMsg = "需要验证码"; } else if (errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_ERROR) { errorMsg = "验证码错误"; } else if (errorCode == ErrorCode.ERROR_TRANSF_ACCESSTOKEN_ERROR) { errorMsg = "accessToken无效"; } Toast.makeText(FanHui.this, errorMsg, Toast.LENGTH_LONG).show(); break; } } }; @Override protected void onDestroy() { super.onDestroy(); stopPlayback(); if (mExecutorService != null) { mExecutorService.shutdown(); } } // 录像信息数据结构 private static class VideoInfo { String id; long startTime; long endTime; int recType; String timeRange; } // 列表适配器 private class PlaybackAdapter extends BaseAdapter { @Override public int getCount() { return mVideoList.size(); } @Override public Object getItem(int position) { return mVideoList.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = getLayoutInflater().inflate(R.layout.video_item_layout, parent, false); holder = new ViewHolder(); holder.timeTextView = convertView.findViewById(R.id.time_text); holder.durationTextView = convertView.findViewById(R.id.duration_text); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } VideoInfo video = mVideoList.get(position); // 计算持续时间(分钟) long durationMinutes = (video.endTime - video.startTime) / (1000 * 60); holder.timeTextView.setText(video.timeRange); holder.durationTextView.setText(durationMinutes + "分钟"); return convertView; } class ViewHolder { TextView timeTextView; TextView durationTextView; } } }
最新发布
06-26
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

talk_8

真诚赞赏,手有余香

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值