Kotlin项目实战之手机影音---处理mv界面条目点击事件、视频播放处理、响应应用外视频播放请求

本文主要是介绍Kotlin项目实战之手机影音---处理mv界面条目点击事件、视频播放处理、响应应用外视频播放请求,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言:

接着上一次Kotlin项目实战之手机影音---加载mv界面区域数据、mv界面viewpager适配、tablayout适配、mv每一个界面列表显示的功能继续往下学习,在上一次由于在网上找的数据接口挂了,重新又找了一个能用的接口网易云音乐 NodeJS 版 API,这里回忆一下具体用法,不然项目启动时看不到数据很受打击:

1、首先启动node服务器:

进入到官方的源码,然后启动既可:

2、更改本机的ip:

此时要注意了,由于是运行在手机上,不是在电脑上,在APP的访问地址域名是不能用localhost的,需要改成本机的ip地址,也就是改它:

由于本机ip地址是会随着网络变化经常变的,所以在学习时一定要注意它的变化,及时进行更新调整。

处理mv界面条目点击事件:

效果预览:

接下来则处理MV列表的点击播放功能,预期的效果是:

具体实现:

1、给Adapter增加点击事件:

这块都比较熟了,由于我们列表是使用的RecylerView来实现的,它不像ListView一样有现成的API可以监听列表的点击,需要给Item View进行事件监听,然后再自己定义接口回调到界面上,具体做法就是到Adapter中:

它里面的onBindViewHolder()中进行rootView的事件监听:

其中这里复习一下Kotlin的语法,为啥这里可以使用大括号?

关于这块可以参考Kotlin项目实战之手机影音---基类抽取、欢迎界面、抽取startactivityandfinish、主界面布局 - cexo - 博客园之前的详细说明,好接下来则需要定义一个回调方法,这里用两种方式对比着学习。

传统回调实现:

这个就不过多解释了,先定义个接口,然后在里面进行调用既可:

很顺其自然对吧,但是对于Kotlin来说回调其实可以更加简便,所以下面来看一下Kotlin对于回调可以如何来定义?

Kotlin回调实现:

在我们传统定义一个回调时其方法是必须定义在一个类当中的对吧?

但是在Kotlin中就不一样了,函数和类都是一等公民,函数可以独立存在的,所以咱们可以更加简便的来定义回调方法了,如下:

然后使用时如下:

还有另外一种调用方式:

其实熟悉java8也有类似的效果,也是将函数提升为一等公民了。

空安全问题: 

在继续往下编写之前,突然想到个东东,就是前几天看我的csdn的博客上有个网友在一篇Flutter的文章Flutter项目实操---资讯、发布动弹_webor2006的博客-CSDN博客中提了个问题:

其中Flutter也有空安全机制,跟Kotlin类似,这里针对这个问题借着这块代码也来稍加说明一下,其实也就是几种情况,一种是变量为空则加个?既可:

另外如果你在使用变量时,这样用可能会报错对吧:

此时,你必须按要求来处理,第一种是你认为该变量不可能为空,此时可以这样用:

但是此时要注意了,这是你自认为的,如果真的变量为空那么程序肯定就崩溃了,所以平常用它时一定要自己来确保空的问题,另外还有一种比较友好的写法就是我们目前所使用的:

再配合着let【关于let扩展函数的使用可以参考博文阅读密码验证 - 博客园】,也就是如果listener为空,那么它里面的方法是不会执行的,这就保证了一个空安全判断的问题了,而对于Flutter的空安全几乎类似,可能语法有些些不同,度娘一下也很容易理解,这里就顺便回答一下该网友的提问了~~

2、MvPagerFragment来注册点击事件回调监听:

这里运行看一下能否正常的监听到点击的条目:

条目点击跳转到播放界面:

准备Activity:

使用anko库实现Intent的跳转的问题说明:

接下来咱们可以处理一下界面的跳转,通常我们直接使用startActivity来进行跳转:

这里扩展个知识吧,其实可以用一个开源的库来简化调转代码,叫anko,地址为Issues · Kotlin/anko · GitHub,不过打开你会发现,官网已经提示该库已经被废弃了:

其实不影响使用,又可以当作自己一个知识面的扩展,假如你在某个项目中会碰到呢,所以这里还是用一下它,对于anko库它其实包含以下几个应用:

都是来帮我们简化平常的一些调用的,这里定位到Intents:

可能有人会说了,这么一个简单的代码也有必要使用三方库么?怎么说呢,三方库的产生都是有目的的,要不是为了性能,要不就是为了代码更加简洁方便提高咱们的开发效率,我觉得三方库不管你项目有没有用到,可以认识一下,反正现在是学习,多多扩展眼界总是好的,至于要不要用到你的项目中,这块就根据自己的意愿来了,好,既然要用它,则需要添加依赖到工程中,其实在之前Kotlin项目实战之手机影音---项目介绍、项目启动 - cexo - 博客园已经添加进工程了: 

下面直接用一下,你会发现用不了:

这是因为该库只对support的Fragment进行了方法扩展,对于Koltin来说要想达到一个封装通用的作法都是采用对系统类进行一个方法扩展,可以看一下这个startActivity的实现:

所以,结论就是还是采用传统的方式来进行Activity的跳转吧。。那,既然没用了你还写出来干嘛?一是扩宽自己的知识面,二是知道该库存在的问题,三是重新提一下它,因为它还是有使用场景的,比如目前toast的还是可以用的:

为啥,因为它是基于Context进行的系统扩展:

对于Kotlin来说扩展方法这个技巧一定要学会,在平常开发中你基于一些类的扩展可以大大提高开发效率。

所以下面代码还是用传统方式来进行跳转,很显然跳转是需要将当前点击的实体传过去的,但是对于咱们目前的item bean来说有很多在播放界面用不到的属性:

所以,为了传递的简洁,这里再封装一个新的Bean用来进行数据传递,如下:

package com.kotlin.musicplayer.modelimport android.os.Parcel
import android.os.Parcelable/*** 传递给视频播放界面的bean类*/
data class VideoPlayBean(var id: Int, var title: String?, var url: String?) : Parcelable {constructor(parcel: Parcel) : this(parcel.readInt(),parcel.readString(),parcel.readString())override fun writeToParcel(parcel: Parcel, flags: Int) {parcel.writeInt(id)parcel.writeString(title)parcel.writeString(url)}override fun describeContents(): Int {return 0}companion object CREATOR : Parcelable.Creator<VideoPlayBean> {override fun createFromParcel(parcel: Parcel): VideoPlayBean {return VideoPlayBean(parcel)}override fun newArray(size: Int): Array<VideoPlayBean?> {return arrayOfNulls(size)}}}

然后跳转代码如下:

其中mv的地址写死了http://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4,本身网上找的API数据不全,这里能正常播就成,不纠结是不是真实有效。

然后在播放界面就可以进行参数接收了,这里打印一下,看参数接收是否一切正常?

运行:

视频播放处理:

接下来就是处理视频播放了,通常也是基于一些三方的框架来傻瓜式的集成,世面上有多少视频播放的开源框架,目前我司项目中使用的是GitHub - CarGuo/GSYVideoPlayer: 视频播放器这款,还是很火的,而这里学习采用另一款https://github.com/Jzvd/JZVideo,节操播入器,具体集成这里就不多说明了,直接按照官网来集成既可,下面将其集成到咱们工程中。

1、添加库依赖:

2、添加布局:

4、设置视频地址、标题:

5、生命周期控制:

6、运行:

接下来运行看一下,发现报错了。。

e: /Users/xiongwei/.gradle/caches/transforms-2/files-2.1/978bdf9bc4b3844ec46f4a1babbe02fe/jetified-jiaozivideoplayer-7.7.0-api.jar!/META-INF/jiaozivideoplayer_release.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.5.1, expected version is 1.1.16.

而且在类上IDE有个提示:

网上搜了一下Error:Kotlin: Module was compiled with an incompatible version of Kotlin. The binary version of its_丧尸爱吃辣的博客-CSDN博客,有说重启一下kotlin插件既可:

发现不好使。。于是在这贴子的评论处又找到一个新的解决方案:Flutter Android 打包报错:Module was compiled with an incompatible version of Kotlin. The binary versi... - 简书,也就是更新一下Kotlin的版本,目前项目用的版本为:

改为:

嗯,貌似可以运行了,此时看一下效果:

此时点击播放时,发现APP崩溃了。。

又度娘一下[Android] java.lang.ClassCastException: Bootstrap method returned null问题处理_SecularBird的专栏-CSDN博客,原来是没有指定jdk的版本为1.8,gradle中指定一下:

再运行看一下,不报错了,但是提示视频播放不了,是因为视频的地址有问题,这里在网上又换了一个地址:用来测试的在线小视频url地址_Mencre的博客-CSDN博客_视频url,视频地址为:https://v-cdn.zjol.com.cn/280443.mp4,具体视频源这里可以自行找找,很有可能之后就不能用了,再运行:

响应应用外视频播放请求:

效果:

对于一款视频播放器,肯定是要支持本地视频打开时可以用咱们的软件来进行播放对吧,效果如下:

关于这块的实现其实也不难,也就是对于Intent的进行一个处理,下面具体来实现一下。

实现:

1、配置intent-filter:

<intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="http" /><data android:scheme="https" /><data android:mimeType="video/mp4" /><data android:mimeType="video/3gp" /><data android:mimeType="video/3gpp" /><data android:mimeType="video/3gpp2" />
</intent-filter>

关于这个fitler可以网上找一下,此时咱们本地找一个视频,打开就可以在列表中出现咱们的应用了:

 当然目前还打不开,因为还没有做相关数据的处理。

2、处理应用外的本地视频请求数据:

这里其实可以先来打印一下本地视频打开来自intent的视频地址:

2021-10-16 05:16:00.004 19194-19194/com.kotlin.musicplayer I/System.out: data=content://com.android.fileexplorer.myprovider/external_files/280443.mp4

 而要访问sdcard内容,肯定需要加权限:

接下来处理播放逻辑:

发现播不了。。为啥呢?因为我手机是9.0的,而在Android7.0以后对于sdcard上的路径都是以content开头的,关于这块可以参考Android7.0sdcard文件访问问题_divaid的博客-CSDN博客,对于我本地的视频路径应该是/storage/emulated/0/280443.mp4,而目前从intent中读取的是content://com.android.fileexplorer.myprovider/external_files/280443.mp4,很明显在7.0以上手机上需要进行处理一下,需要根据content的路径来查找真实的sdcard路径,而方法我是在网上搜到的android uri 解析获取文件真实路径(兼容7.0+)_JokAr-CSDN博客_android uri 获取文件路径,这里将其封装成一个工具方法便于之后在其它界面也可以使用:

而对于工具方法一般都会将类设计成单例的对吧,在Kotlin中怎么来弄呢?代码如下:

package com.kotlin.musicplayer.utilsimport android.content.ContentResolver
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import android.text.TextUtils
import java.io.*object FileUtil {fun getFileFromUri(uri: Uri?, context: Context?): File? {return if (uri == null) {null} else when (uri.getScheme()) {"content" -> getFileFromContentUri(uri, context)"file" -> File(uri.getPath())else -> null}}/*** Gets the corresponding path to a file from the given content:// URI** @param contentUri The content:// URI to find the file path from* @param context    Context* @return the file path as a string*/private fun getFileFromContentUri(contentUri: Uri?, context: Context?): File? {if (contentUri == null) {return null}var file: File? = nullvar filePath: String? = nullval fileName: Stringval filePathColumn =arrayOf(MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME)val contentResolver: ContentResolver? = context?.getContentResolver()val cursor: Cursor? = contentResolver?.query(contentUri, filePathColumn, null,null, null)if (cursor != null) {cursor.moveToFirst()try {filePath = cursor.getString(cursor.getColumnIndex(filePathColumn[0]))} catch (e: Exception) {}fileName = cursor.getString(cursor.getColumnIndex(filePathColumn[1]))cursor.close()if (!TextUtils.isEmpty(filePath)) {file = File(filePath)}if (!file!!.exists() || file.length() <= 0 || TextUtils.isEmpty(filePath)) {filePath = getPathFromInputStreamUri(context, contentUri, fileName)}if (!TextUtils.isEmpty(filePath)) {file = File(filePath)}}return file}/*** 用流拷贝文件一份到自己APP目录下** @param context* @param uri* @param fileName* @return*/fun getPathFromInputStreamUri(context: Context?, uri: Uri, fileName: String): String? {var inputStream: InputStream? = nullvar filePath: String? = nullif (uri.authority != null) {try {inputStream = context?.contentResolver?.openInputStream(uri)val file = createTemporalFileFrom(context, inputStream, fileName)filePath = file!!.path} catch (e: java.lang.Exception) {} finally {try {if (inputStream != null) {inputStream.close()}} catch (e: java.lang.Exception) {}}}return filePath}@Throws(IOException::class)private fun createTemporalFileFrom(context: Context?,inputStream: InputStream?,fileName: String): File? {var targetFile: File? = nullif (inputStream != null) {var read: Intval buffer = ByteArray(8 * 1024)//自己定义拷贝文件路径targetFile = File(context?.getCacheDir(), fileName)if (targetFile.exists()) {targetFile.delete()}val outputStream: OutputStream = FileOutputStream(targetFile)while (inputStream.read(buffer).also { read = it } != -1) {outputStream.write(buffer, 0, read)}outputStream.flush()try {outputStream.close()} catch (e: IOException) {e.printStackTrace()}}return targetFile}
}

一个object声明就可以了,为啥?其实将它可以转换成java类就明白了:

然后再来修改一下咱们的视频处理代码:

此时就可以来运行看一下效果了,如果你是6.0以上手机,你会发现会报sdcard权限问题:

这是因为对于sdcard权限在6.0以后是需要我们主动申请才行的,这里为了方便,先手动进应用详情中打开它:

因为这块在之后会专门处理的,这里先略过,另外关于动态权限申请框架的搭建可以参考我之前写过的这篇Android9.0动态运行时权限源码分析及封装改造<一>-----运行时权限名词解释、权限检测源码分析 - cexo - 博客园,完整的记录了整个申请过程。好,接下来看一下最终效果:

3、处理应用外的网络视频请求数据:

对于应用外的视频应该咱们播放器还支持网络的对吧,所以接下来处理一下。

1、先新建一个module准备网络视频跳转:

这里应该是在另一个app中来跳到咱们这款播放器app中对吧,这里以新建module的形式来准备这个测试跳转app:

然后搞个在线视频的测试入口,点击则跳转一下,具体如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"tools:context=".MainActivity"><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:onClick="onclick"android:text="打开网络视频" /></RelativeLayout>

好,此时运行在手机上:

2、处理外部网络视频的播放逻辑:

其处理也比较简单:

3、运行:

最后咱们运行看一下:

播放器下面增加ViewPager滑动效果:

效果:

对于这个视频播放界面下面还空出一截对吧,接下来则需要来完善它,效果也很简单:

也就是一个tab滑动切换的效果,由于API目前网上也没找到比较合适的,这里就是占个位,具体内容这里就忽略了,其实就是一些视频的简介之类的,纯展示用的,比较简单,这里快速过一下。

实现:

1、布局准备:

<?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"><cn.jzvd.JzvdStdandroid:id="@+id/jz_video"android:layout_width="match_parent"android:layout_height="200dp" /><RadioGroupandroid:id="@+id/rg"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_margin="20dp"android:orientation="horizontal"><RadioButtonandroid:id="@+id/rb1"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:background="@drawable/mv_description"android:button="@null"android:checked="true" /><RadioButtonandroid:id="@+id/rb2"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:background="@drawable/mv_comment"android:button="@null" /><RadioButtonandroid:id="@+id/rb3"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:background="@drawable/mv_relative"android:button="@null" /></RadioGroup><androidx.viewpager.widget.ViewPagerandroid:id="@+id/viewPager"android:layout_width="match_parent"android:layout_height="match_parent" />
</LinearLayout>

其中RadioButton有三个背景资源,如下:

mv_comment.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:drawable="@mipmap/player_comment_p" android:state_checked="true"/><item android:drawable="@mipmap/player_comment"/>
</selector>

mv_description.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:drawable="@mipmap/player_mv_p" android:state_checked="true"/><item android:drawable="@mipmap/player_mv"/>
</selector>

mv_relative.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:drawable="@mipmap/player_relative_mv_p" android:state_checked="true"/><item android:drawable="@mipmap/player_relative_mv"/>
</selector>

涉及到的图片如下:

player_comment.png:

player_comment_p.png:

player_mv.png:

player_mv_p.png:

player_relative_mv.png:

player_relative_mv_p.png:

2、 准备Fragment:

由于就是占一个位,这里用一个Fragment既可,如下:

3、处理切换逻辑:

这块直接把代码贴出来了,都比较熟了,Kotlin语法也比较简单:

package com.kotlin.musicplayer.ui.activityimport androidx.viewpager.widget.ViewPager
import cn.jzvd.Jzvd
import com.kotlin.musicplayer.R
import com.kotlin.musicplayer.adapter.VideoPagerAdapter
import com.kotlin.musicplayer.base.BaseActivity
import com.kotlin.musicplayer.model.VideoPlayBean
import com.kotlin.musicplayer.utils.FileUtil
import kotlinx.android.synthetic.main.activity_video_player.*/*** 视频播放详情界面*/
class VideoPlayerActivity : BaseActivity() {override fun getLayoutId(): Int {return R.layout.activity_video_player}override fun initData() {super.initData()val data = intent.dataprintln("data=$data")if (data == null) {//应用内视频处理val videoPlayBean = intent.getParcelableExtra<VideoPlayBean>("item")jz_video.setUp(videoPlayBean.url, videoPlayBean.title)} else {//应用外视频处理if (data.toString().startsWith("http")) {//网络视频jz_video.setUp(data.toString(), data.toString())} else {//本地视频val filePath = FileUtil.getFileFromUri(data, this)?.absolutePathjz_video.setUp(filePath, filePath)}}}override fun onBackPressed() {if (Jzvd.backPress()) {return}super.onBackPressed()}override fun onPause() {super.onPause()Jzvd.releaseAllVideos()}override fun initListeners() {//适配viewpagerviewPager.adapter = VideoPagerAdapter(supportFragmentManager)//radiogroup选中监听rg.setOnCheckedChangeListener { radioGroup, i ->when (i) {R.id.rb1 -> viewPager.setCurrentItem(0)R.id.rb2 -> viewPager.setCurrentItem(1)R.id.rb3 -> viewPager.setCurrentItem(2)}}//viewpager选中状态监听viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {/*** 滑动状态改变的回调*/override fun onPageScrollStateChanged(state: Int) {}/*** 滑动回调*/override fun onPageScrolled(position: Int,positionOffset: Float,positionOffsetPixels: Int) {}/*** 选中状态改变回调*/override fun onPageSelected(position: Int) {when (position) {0 -> rg.check(R.id.rb1)1 -> rg.check(R.id.rb2)2 -> rg.check(R.id.rb3)}}})}
}

4、运行:

关注个人公众号,获得实时推送

这篇关于Kotlin项目实战之手机影音---处理mv界面条目点击事件、视频播放处理、响应应用外视频播放请求的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/798012

相关文章

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

python处理带有时区的日期和时间数据

《python处理带有时区的日期和时间数据》这篇文章主要为大家详细介绍了如何在Python中使用pytz库处理时区信息,包括获取当前UTC时间,转换为特定时区等,有需要的小伙伴可以参考一下... 目录时区基本信息python datetime使用timezonepandas处理时区数据知识延展时区基本信息

C语言中位操作的实际应用举例

《C语言中位操作的实际应用举例》:本文主要介绍C语言中位操作的实际应用,总结了位操作的使用场景,并指出了需要注意的问题,如可读性、平台依赖性和溢出风险,文中通过代码介绍的非常详细,需要的朋友可以参... 目录1. 嵌入式系统与硬件寄存器操作2. 网络协议解析3. 图像处理与颜色编码4. 高效处理布尔标志集合

SpringBoot请求参数接收控制指南分享

《SpringBoot请求参数接收控制指南分享》:本文主要介绍SpringBoot请求参数接收控制指南,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring Boot 请求参数接收控制指南1. 概述2. 有注解时参数接收方式对比3. 无注解时接收参数默认位置

SpringBoot项目中报错The field screenShot exceeds its maximum permitted size of 1048576 bytes.的问题及解决

《SpringBoot项目中报错ThefieldscreenShotexceedsitsmaximumpermittedsizeof1048576bytes.的问题及解决》这篇文章... 目录项目场景问题描述原因分析解决方案总结项目场景javascript提示:项目相关背景:项目场景:基于Spring

Python Transformers库(NLP处理库)案例代码讲解

《PythonTransformers库(NLP处理库)案例代码讲解》本文介绍transformers库的全面讲解,包含基础知识、高级用法、案例代码及学习路径,内容经过组织,适合不同阶段的学习者,对... 目录一、基础知识1. Transformers 库简介2. 安装与环境配置3. 快速上手示例二、核心模

解决Maven项目idea找不到本地仓库jar包问题以及使用mvn install:install-file

《解决Maven项目idea找不到本地仓库jar包问题以及使用mvninstall:install-file》:本文主要介绍解决Maven项目idea找不到本地仓库jar包问题以及使用mvnin... 目录Maven项目idea找不到本地仓库jar包以及使用mvn install:install-file基

一文详解Java异常处理你都了解哪些知识

《一文详解Java异常处理你都了解哪些知识》:本文主要介绍Java异常处理的相关资料,包括异常的分类、捕获和处理异常的语法、常见的异常类型以及自定义异常的实现,文中通过代码介绍的非常详细,需要的朋... 目录前言一、什么是异常二、异常的分类2.1 受检异常2.2 非受检异常三、异常处理的语法3.1 try-

Spring 请求之传递 JSON 数据的操作方法

《Spring请求之传递JSON数据的操作方法》JSON就是一种数据格式,有自己的格式和语法,使用文本表示一个对象或数组的信息,因此JSON本质是字符串,主要负责在不同的语言中数据传递和交换,这... 目录jsON 概念JSON 语法JSON 的语法JSON 的两种结构JSON 字符串和 Java 对象互转