前言:
接着上一次https://www.cnblogs.com/webor2006/p/15145953.html的功能繼續往下學習,在上一次由於在網上找的數據接口掛了,重新又找了一個能用的接口https://neteasecloudmusicapi.vercel.app/#/,這里回憶一下具體用法,不然項目啟動時看不到數據很受打擊:
1、首先啟動node服務器:
進入到官方的源碼,然后啟動既可:

2、更改本機的ip:
此時要注意了,由於是運行在手機上,不是在電腦上,在APP的訪問地址域名是不能用localhost的,需要改成本機的ip地址,也就是改它:
由於本機ip地址是會隨着網絡變化經常變的,所以在學習時一定要注意它的變化,及時進行更新調整。
處理mv界面條目點擊事件:
效果預覽:
接下來則處理MV列表的點擊播放功能,預期的效果是:
具體實現:
1、給Adapter增加點擊事件:
這塊都比較熟了,由於我們列表是使用的RecylerView來實現的,它不像ListView一樣有現成的API可以監聽列表的點擊,需要給Item View進行事件監聽,然后再自己定義接口回調到界面上,具體做法就是到Adapter中:

它里面的onBindViewHolder()中進行rootView的事件監聽:
其中這里復習一下Kotlin的語法,為啥這里可以使用大括號?

關於這塊可以參考https://www.cnblogs.com/webor2006/p/12622874.html之前的詳細說明,好接下來則需要定義一個回調方法,這里用兩種方式對比着學習。
傳統回調實現:
這個就不過多解釋了,先定義個接口,然后在里面進行調用既可:

很順其自然對吧,但是對於Kotlin來說回調其實可以更加簡便,所以下面來看一下Kotlin對於回調可以如何來定義?
Kotlin回調實現:
在我們傳統定義一個回調時其方法是必須定義在一個類當中的對吧?

但是在Kotlin中就不一樣了,函數和類都是一等公民,函數可以獨立存在的,所以咱們可以更加簡便的來定義回調方法了,如下:

然后使用時如下:

還有另外一種調用方式:

其實熟悉java8也有類似的效果,也是將函數提升為一等公民了。
空安全問題:
在繼續往下編寫之前,突然想到個東東,就是前幾天看我的csdn的博客上有個網友在一篇Flutter的文章https://blog.csdn.net/webor2006/article/details/119747772中提了個問題:

其中Flutter也有空安全機制,跟Kotlin類似,這里針對這個問題借着這塊代碼也來稍加說明一下,其實也就是幾種情況,一種是變量為空則加個?既可:

另外如果你在使用變量時,這樣用可能會報錯對吧:

此時,你必須按要求來處理,第一種是你認為該變量不可能為空,此時可以這樣用:

但是此時要注意了,這是你自認為的,如果真的變量為空那么程序肯定就崩潰了,所以平常用它時一定要自己來確保空的問題,另外還有一種比較友好的寫法就是我們目前所使用的:

再配合着let【關於let擴展函數的使用可以參考https://www.cnblogs.com/webor2006/p/13529865.html】,也就是如果listener為空,那么它里面的方法是不會執行的,這就保證了一個空安全判斷的問題了,而對於Flutter的空安全幾乎類似,可能語法有些些不同,度娘一下也很容易理解,這里就順便回答一下該網友的提問了~~
2、MvPagerFragment來注冊點擊事件回調監聽:

這里運行看一下能否正常的監聽到點擊的條目:

條目點擊跳轉到播放界面:
准備Activity:


使用anko庫實現Intent的跳轉的問題說明:
接下來咱們可以處理一下界面的跳轉,通常我們直接使用startActivity來進行跳轉:

這里擴展個知識吧,其實可以用一個開源的庫來簡化調轉代碼,叫anko,地址為https://github.com/Kotlin/anko/issues,不過打開你會發現,官網已經提示該庫已經被廢棄了:

其實不影響使用,又可以當作自己一個知識面的擴展,假如你在某個項目中會碰到呢,所以這里還是用一下它,對於anko庫它其實包含以下幾個應用:
都是來幫我們簡化平常的一些調用的,這里定位到Intents:

可能有人會說了,這么一個簡單的代碼也有必要使用三方庫么?怎么說呢,三方庫的產生都是有目的的,要不是為了性能,要不就是為了代碼更加簡潔方便提高咱們的開發效率,我覺得三方庫不管你項目有沒有用到,可以認識一下,反正現在是學習,多多擴展眼界總是好的,至於要不要用到你的項目中,這塊就根據自己的意願來了,好,既然要用它,則需要添加依賴到工程中,其實在之前https://www.cnblogs.com/webor2006/p/12612286.html已經添加進工程了: 
下面直接用一下,你會發現用不了:
這是因為該庫只對support的Fragment進行了方法擴展,對於Koltin來說要想達到一個封裝通用的作法都是采用對系統類進行一個方法擴展,可以看一下這個startActivity的實現:

所以,結論就是還是采用傳統的方式來進行Activity的跳轉吧。。那,既然沒用了你還寫出來干嘛?一是擴寬自己的知識面,二是知道該庫存在的問題,三是重新提一下它,因為它還是有使用場景的,比如目前toast的還是可以用的:
為啥,因為它是基於Context進行的系統擴展:

對於Kotlin來說擴展方法這個技巧一定要學會,在平常開發中你基於一些類的擴展可以大大提高開發效率。
所以下面代碼還是用傳統方式來進行跳轉,很顯然跳轉是需要將當前點擊的實體傳過去的,但是對於咱們目前的item bean來說有很多在播放界面用不到的屬性:

所以,為了傳遞的簡潔,這里再封裝一個新的Bean用來進行數據傳遞,如下:

package com.kotlin.musicplayer.model import 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數據不全,這里能正常播就成,不糾結是不是真實有效。
然后在播放界面就可以進行參數接收了,這里打印一下,看參數接收是否一切正常?
運行:

視頻播放處理:
接下來就是處理視頻播放了,通常也是基於一些三方的框架來傻瓜式的集成,世面上有多少視頻播放的開源框架,目前我司項目中使用的是https://github.com/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有個提示:

網上搜了一下https://blog.csdn.net/qq_37875500/article/details/117418214,有說重啟一下kotlin插件既可:

發現不好使。。於是在這貼子的評論處又找到一個新的解決方案:https://www.jianshu.com/p/3d4abaef8163,也就是更新一下Kotlin的版本,目前項目用的版本為:
改為:

嗯,貌似可以運行了,此時看一下效果:

此時點擊播放時,發現APP崩潰了。。

又度娘一下https://blog.csdn.net/yao_zhuang/article/details/107095665,原來是沒有指定jdk的版本為1.8,gradle中指定一下:

再運行看一下,不報錯了,但是提示視頻播放不了,是因為視頻的地址有問題,這里在網上又換了一個地址:https://blog.csdn.net/qq_17497931/article/details/80824328,視頻地址為: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開頭的,關於這塊可以參考https://blog.csdn.net/divaid/article/details/79419858,對於我本地的視頻路徑應該是/storage/emulated/0/280443.mp4,而目前從intent中讀取的是content://com.android.fileexplorer.myprovider/external_files/280443.mp4,很明顯在7.0以上手機上需要進行處理一下,需要根據content的路徑來查找真實的sdcard路徑,而方法我是在網上搜到的https://blog.csdn.net/a1018875550/article/details/82957333?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-8.no_search_link&spm=1001.2101.3001.4242,這里將其封裝成一個工具方法便於之后在其它界面也可以使用:

而對於工具方法一般都會將類設計成單例的對吧,在Kotlin中怎么來弄呢?代碼如下:
package com.kotlin.musicplayer.utils import 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? = null var filePath: String? = null val fileName: String val 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? = null var filePath: String? = null if (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? = null if (inputStream != null) { var read: Int val 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以后是需要我們主動申請才行的,這里為了方便,先手動進應用詳情中打開它:

因為這塊在之后會專門處理的,這里先略過,另外關於動態權限申請框架的搭建可以參考我之前寫過的這篇https://www.cnblogs.com/webor2006/p/12757460.html,完整的記錄了整個申請過程。好,接下來看一下最終效果:

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"> <Button android: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.JzvdStd android:id="@+id/jz_video" android:layout_width="match_parent" android:layout_height="200dp" /> <RadioGroup android:id="@+id/rg" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="20dp" android:orientation="horizontal"> <RadioButton android: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" /> <RadioButton android:id="@+id/rb2" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:background="@drawable/mv_comment" android:button="@null" /> <RadioButton android: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.ViewPager android: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.activity import 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.data println("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)?.absolutePath jz_video.setUp( filePath, filePath ) } } } override fun onBackPressed() { if (Jzvd.backPress()) { return } super.onBackPressed() } override fun onPause() { super.onPause() Jzvd.releaseAllVideos() } override fun initListeners() { //適配viewpager viewPager.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、運行:

