【Kotlin】FarawayPlayer
下载预览
- Github:https://github.com/kirikaTowa/FarawayPlayer 部分网络接口可能已过期
- 补充资料:MediaStore.MediaColumns 与 MediaStore.Audio.AudioColumns,从字段来看,父MediaColumn更加的全面,子AudioColumns多了id与key等字段。
2.1 从官网我们可以看到,AudioColumns是继承自MediaColumns的,
一:boat
- 采用MVP框架
该项目来自黑马程序员的视频教学。由于原网络接口不太稳定,找了新的接口及类封装方式。按照自己的喜好体系化梳理结构。本人的每次提交的gihub源码都可以在对应一级标题中跳转查看。
- 使用Genymotion(或直连手机)
- anko库
1)封装过了一些SDK方法,使调用更加的方便toast(R.string.message)取代 Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
implementation “org.jetbrains.anko:anko:0.10.8”`
- github
1)git add .
2)git commit -m "First Commit."
3)git push origin master
- jetpack安卓官方
二:First Cmt【okhttp获取数据并显示】
1.项目结构
- M层:
1)第一个Model目录目前存放了一个bean类 接收回调的网络数据
2)第五个Util目录目前存放了一个utils - V层:
1)第二个Base目录中分别定义了Activity和Fragment基类
2)第四个UI目录都继承自Base类
3)第六个Widget类型,小组件模块
a.Recycleview的基础组件
b.一些通用组 - P层:
1)第一个Adapter是做了Home页的RecycleView绑定
2.抽象类BaseActivity分析
1.onCreate三个普通方法子类可不复写
2.由于目前还不知道要传入那个layout 可写一个抽象方法子类实现通过id传 获取对应layout的id进行设置ContentView
//创建抽象方法以供获取Layoutabstract fun getLayoutid():Int
3.对消息处理最好启用线程 toast容易报错
protected fun myToast(msg:String){runOnUiThread{toast(msg)}}
4.由于做了入场动画效果,可以封装一个方法用于开启新活动并自动关闭当前活动(这里的写法是调用的anko库封装的Intent)
内联(reified与内联相当于具体化一个泛型T并getName)inline标记可以致辞reified获取泛型类型【面向BaseActivity子类都开放】
inline fun <reified T:BaseActivity>startActivityAndFinish(){startActivity<T>()finish()}
2.5.BaseFragment抽取
具有三个较为明显的父类方法
onCreate(普通初始化)
1)写一个init()方法,不一定要实现
2)加了个debug(BACT也可以加)onCreateView
1)用于实现静态布局初始化这里return了一个方法
2)这个自己写的方法是抽象的,子类调用返回其ViewoncreateActivity方法,
1)调用 initListener()和 initData()进行动态数据绑定
2)最后封装一个Mytoast方法处理线程问题
3.入场动画分析
1.首先继承了Base方法 这里就不复写oncreate方法,复写getlayoutid方法传入动画layout,配置全屏动画并且设置无ActionBar 追加全屏显示
<!-- Base application theme. --><style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"><!-- Customize your theme here. --><item name="colorPrimary">@color/colorPrimary</item><item name="colorPrimaryDark">@color/colorPrimaryDark</item><item name="colorAccent">@color/colorAccent</item></style><style name="AppTheme.FullScreen"><item name="windowNoTitle">true</item><item name="android:windowFullscreen">true</item></style><style name="pop"><item name="android:windowEnterAnimation">@anim/pop_enter</item><item name="android:windowExitAnimation">@anim/pop_exit</item></style>
<activityandroid:name=".ui.activity.SplashActivity"android:theme="@style/AppTheme.FullScreen">
2.复写initdatas方法,利用ViewCompat的静态方法进行数据拉伸回来及持续时间(layout设置放大为1.5倍
ViewCompat.animate(imageView).scaleX(1.0f).scaleY(1.0f).setListener(this).setDuration(2000)
(这里的imageview是)
3.ViewPropertyAnimatorListener继承接口回调进入主函数,销毁动画界面
4.主界面布局分析
- 首先观察需求
1)顶部是一个ToolBar文件
2)底部是一个bottombar组件
3)中间用了权重为1的FrameLayout容器 【bottombar组件要用】 - 实现布局方法
1)写一个xml的layout文件,MLayout include就行
<include layout="@layout/toolbar"/>
2)底部使用Bottom组件并指定tabs.xml
implementation ‘com.roughike:bottom-bar:2.3.1’
- 主界面动态适配ToolBar(由于需求三个组件都是动态交互的,所以不能写成静态的)
1)M层Utils包下建立ToolBarManager接口用于管理类
a.新建一个toolbar对象
b.写一个初始化Main主页标题信息
kotlin接口是可以写实现的,java不行
2)MACT中实现接口并继承toolbar对象并惰性加载【确保线程安全和初始化时toolbar时才执行(findviewbyid)】布局toolbar对象,再直接实现无参方法即可(复写在initdata方法里)
override val toolbar by lazy{find<Toolbar>(R.id.tb_toolbar)}
//初始化toolbar查看效果override fun initDate() {initMainToolBar()}
- 为ToolBar添加Manu
1)Menu文件夹中写一个文件menu
2)ToolBar初始化Main主页方法中通过inflate动态R.id.menu生成View
3)设置菜单监听
a.先复写监听ToolBar
b.再监听具体Item内容
c.点击跳转到设置界面(相比于传统写法,这种写法很有意思)
4)java中若接口的方法只有一个,可以直接省去实现接口参数,直接复写方法就好了
toolbar.setOnMenuItemClickListener
//也可以
//a->//构造a进行捕捉it 下面取值$aprintln("item=$it")//it是隐含的选中MenuItemtoolbar.context.startActivity(Intent(toolbar.context,SettingActivity::class.java))true}
toolbar.setOnMenuItemClickListener(object : Toolbar.OnMenuItemClickListener{override fun onMenuItemClick(item: MenuItem?): Boolean {when(item?.itemId){R.id.setting-> {toolbar.context.startActivity(Intent(toolbar.context,SettingActivity::class.java))}return true;}
- 为新建的设置动态添加项目
1)layout和toolbar的操作是一样的
2)区别在于初始化时initdata时除了加载settingtoolbar显示,还需加载新的控件(按钮控件)
3)运用PreferenceFragment或PreferenceActivity组件实现,由于SettingActivity已经继承了BaseActivity,所以这里使用Fragment - 创建SettingFragment继承PreferenceFragment
1)既然是Fragment显示需要onCreateView方法内部
addPreferencesFromResource(R.xml.setting)
设定该Fragment的布局
2)设定setting文件层级目录
a.PreferenceScreen
b.Preference
c.SwitchPreferen - 将装配好的Fragment引入activity_setting布局中
借助<Fragment
标签,在initdata中
1)通过SharePreference对象保存
2)通过该对象的getBoolean进行选中判断 - 类似的 主界面的BottomBar切换
- MACT中initlistener中设置bottombar监听(同样含it,进行Container替换)
- M层Utils包下设置一个FragmentUtils切换类,根据id获得相应的Fragment
Setting中只有一个布局可以直接layout设定,而MainLayout是根据选择replace的,要设置对应的Fragment进行替换
5.建立对应的Fragment与对应的xml文件
- 将要装填条目,所以布局会用到RecycleView(大)
- 必然建立一个adapter进行适配(小条目适配)
1)adapter三个方法
2)onBindViewHolder绑定的布局中条目较多时,widget封装类HomeItemView继承RleativeLayout,复写三个相关构造方法,定义次构造器可以不写方法体,无{},但要调主构造方法,这样不论调什么方法都会执行init{}
3)init初始化,inflate动态加载一个布局(item_home.xml)(小条目界面)用了卡片布局加载,看起来挺舒服的
implementation ‘androidx.cardview:cardview:1.0.0’
6.发送网络请求(使用okhttp自封框架)
安卓8.0需要对http类型特殊处理,https正常
方法1
e.printStackTrace()和log调试很好用,
e.printStackTrace()可能会造成死锁
compile ‘com.squareup.okhttp3:okhttp:3.8.1’
- 既然是获取数据,HomeFragment就继承父类的initData(),写一个方法(loadDatas)专门调用获取网络数据功能填loadData()
- okhttp的使用
1)需要一个路径
2)需要一个okhttpclient对象
3)request请求,将path传入
4)通过client发送request并进行结果回调
a.成功的回调
a1)拿到string数据val result = response?.body()?.string()
b2)postman跑接口,观察xml还是json格式,转换后写一个类用于对应储存
b.失败的回调(提示交互) - 拿到数据,Homeadapter进行界面更新
1)定义list泛型<>
2)更新,clean,addall,刷新
3)Android界面的更新只能在主线程总进行,所有在HomeFragment的成功回调中还得使用handler或runOnMainThread
4)Util包下封装一个方法ThreadUtil静态类(object),写一个方法运行主线程即可
5)回到HFment中,静态调用主线程方法,复写run方法,内部使用之前适配惰性加载的adapter对象进行 adapter类的内部刷新
6)来到adapter刷新页面,在onBindViewHolder方法中根据位置进行条目的具体信息通过holder.itemview绑定
a)拿到数据
b)拿到itemview
7)接下来有个是数据对应取出与装载,由于之前写了homeview的类,这边就可以在里面调用赋值了
a.Title
b.author
c.image
8)通过Picasso来将获取到的图片(http流式图片)转化为正常图片显示
implementation ‘com.squareup.picasso:picasso:2.5.2’
7.下拉刷新与上拉加载更多
- 现在用的接口默认加载10条数据,type为起始页。
- 使用刷新控件,google自带,加入fragment_home
SwipeRefreshLayout
将Recycleview包裹住,下拉就有效果了。
1)refreshLayout.setColorSchemeColors(Color.RED, Color.YELLOW, Color.GREEN)
放在fragment的initlistener里动态监听刷新状态颜色。
2)刷新监听,由于要初始化数据,所有监听内部调用 Loaddatas()方法和初始化一样。
3)不管成功失败都要设置刷新控件隐藏refreshLayout.isRefreshing=false 回调一定要用之前封装好的Util中主线程运行类 动态改变View在子线程会报错
。 - 上拉加载更多
1)view显示,在最后一条记录后,用那个圆形转圈圈进度条好看
a.条目数量可以在adapter的getcount方法中获得
b.鉴于这个进度条其他也可能用到,可以封装到widget中
2)新建Loadmoreviw继承自RelativeLayout,inflate一个布局,该布局内是一个<ProgressBar
a.回到homeadapter的onbindview中,复习第四个方法getItemViewType
进行boolean的判断是否到了最后一条
b.这个return的值是onCreateViewHolder()中的viewtype
,内部写一个when或者 if判断调用loadmore或原本的Homeitemview进行条目显示
c.到了条目显示这一步,自然会联动数据的装载onBindViewHolder()
中,加一个判断,是最后一条就不装载,就不用调用方法进行数据绑定了
3)回到,通过okhttp加载更多数据HomeFragment中,initlistener监听列表滑动,复写滑动状态改变的方法有两种,这边使用只触发一次的onScrollStateChanged(其中滚动状态有三种)
4)Recycleview中只有linnerLayout才会有列表的最后一条概念,在列表处于空闲状态监听,判断最后的条目是否为最终可见条目,是的话就要调用loadmore()方法进行more数据加载。
a.loadmore()和loaddatas(),内部方法差不多,只不过改变type的量级
b.到了的刷新列表,loaddatas()中的update()方法内要先clear()堆栈,但loadmore()是绝对不应该clean,可以再写一个loadmore()方法调用,内部addall(list)与notifyDataSetChanged()即可。
二:[Second Cmt]首页MVP封装
View包下创建HomeView接口,负责home界面和presenter交互,HomeFragment继承接口并重写方法。
1)onError(一个错误信息)
2)loadSuccess(拿到list数据)
3)loadmore(拿到list数据)接下来在presenter包下
写一个接口HomePresenter控制数据流 供实现类HomePresenterImpl调用(一个接口一个实现类,明确模块)
1)该接口定义了loaddata和loadmore方法,供实现类继承
2)HomePresenterImpl实现类继承后,将HomeFragment的两个方法拿过来复写,HF调用新建一个实现类对象调用就好了。由于这是p层,加载成功失败的消息要UiI更新
3)V层消息更新,之前的HF继承homeview,在实现类中加主构造方法var一个新HomeVIiew对象,进行实现类与HF绑定。HF中复写了三个方法,利用该对象完成界面的更新。
三:[Third Cmt]网络架构的封装,抽取重复代码
网络架构的抽取封装,MVP框架中,网络操作是在Model层中的,【Second Cmt】中网络操作处于实现类P层,所以要进行抽取,loadmore和loaddata中有大量重复的代码
参照Volley的结构
2.创建net
包
- 定义所有请求的基类MRequest
1)主构造器
a.一个url地址
b. 一个接口对象回调 - 创建responseHandler接口
1)定义了成功和失败方法
2)失败一个反馈,而成功每次返回的结果不一样,指定一个泛型
3)由于每次请求的封装类不同,MRequest也指定一个泛型
open class MRequest<RESPONSE>(val url: String,val handler: ResponseHandler<RESPONSE>)
- 刚刚提到了一个管理类 现在来实现以下
1)创建NetManage类,发送网络请求,由于每次发送请求都要用到这个对象,可以做成单例模式,创建伴生对象并且惰性加载
2)定义一个发送的方法sendRequest方法,并定义泛型,参数写MRequst
fun <RESPONSE>sendRequest(req:MRequest<RESPONSE>)
a.将【FST CMT】6.2的okhttp内容【SCD CMT】封装在Impl里移过来
b.去除val path,基类已经定义了,实现类传一下就好了(req.url)
c.okhttpclient创建一次就好了,拿出来放外面,惰性加载一下
3)更改回调方式主构造器定义了ResponseHandler进行回调
a. 获取失败时的:homeView.onError(e?.message)改为 req.handler.onError(e?.message)
b1.请求成功时Gson这边需要更改,原本类型写死,这边得用泛型,说到泛型,就刚好可以放到MRequest基类中,需求每个界面都要对应的request,那一定也有对应的类型。写一个parseResult
方法··········,调用之后返回一个解析后的集合,储存就行了
b2.MRequest中首先要获取泛型类型【翻译成中文大白话就知道怎么实现的了】
val type=(this.javaClass.genericSuperclass as ParameterizedType).getActualTypeArguments()[0]
b3. 原成功回调homeView.loadMore(userBeanList)
改为req.handler.onSuccess(parseResult)
创建实现类HomeRequest,这种写法很好用(,)里面的两个参数,一个int,一个handler接口对象,继承主类时(,)利用两个参数进行初始化主构造器
例如:class HomeRequest(type:Int,handler: ResponseHandler<HomeItemBeanSuccess>) :MRequest<HomeItemBeanSuccess>(YWJHURLProviderUtils.getHomeUrl(type),handler) { }
- 回到HomePresenterImpl,okhttp框架被拿走此刻调用就行了,loaddata和loadmore就type参数不一样。
a. 定义request,第二个参数,定义匿名内部类(这是真的好用)
a1.匿名内部类实现onError
a2.匿名内部类实现onSuccess
a3.不知道大家还记不记得咱们网络框架原本调用的homeview被req的方法取代了,现在复写方法,调用homeview一波搞定。
a4.值得一提的是咱们换了接口拿到的是大类,这边传的是list,所以回调的resule要getresult,将我们装好list的每条数据类加载进来
b.网络请求管理类发送request(静态调用) - 进一步简化
1)MRequest定义一个方法。内部发送获取this(req)IMPL初始化时直接调用就可以了
2)IMPL中的loaddatas和loadmore,回调接口方面是重合的,第二个参数匿名内部类全部用this替换,通过当前类实现接口就好了。
3)类实现回调是没问题,但是成功回调要判断是初始化还是加载更多
a.解决方法就是将他们分别写个标识符,随方法一起传过去
b.NetManager中回调回来就行了(ps 发现基类主构造器val了之后 ,后面的不复写直接用就可以了)
c.这个时候我们发现这两个常量也可以放到Presenter接口中,但这个kotlin不能直接定义常量,还是用到伴生对象
,impl实现静态调用即可
d.其实直接传1,2也行。不过这样写更安全
** val request=HomeRequest(1,object :ResponseHandler<HomeItemBeanSuccess>{//实现两个方法override fun onError(msg: String?) {//netmanage中处理过线程问题 所以处理时都是在主线程中homeView.onError(msg)}override fun onSuccess(result: HomeItemBeanSuccess) {homeView.loadMore(result.getResult())}}).execute()//匿名内部类**
四:[Fourth Cmt]悦单Fragment封装
1.通过网络架构的封装和基类的抽取后,到这步已经非常简单了,拿接口就行了,值得一提的是kotlin的很多地方用习惯了真的比java更简洁舒服。
- 这次的数据bean类层数很多,泛型定义注意,在获取request阶段传第一层bean类,接收对应的信息
- 成功后,adapter和实现View拿数据该拿啥型写啥型
提到了response有很多层,这次内部封装了list在第三层,所以View中也不必直接用list装起来1.(第一层bean类,浪费空间,也没必要过早的取出来,费时费力 2.省去了实现类与Fragment的写法问题和麻烦)
- 补充接口信息:
1)limit限制条目数
2)offset偏移量,从第几条开始
a)loadmore首次传参 设定limt=3 而我们接口返回数据下标是从(offset=0)开始的 返回三条数据,(0,1,2 那么下次应该为3 ) 打印当前adapter.itemCount=4(这个是从1开始,而且适配了多一条进度条)算上进度条是第四条,所以这种写法presenter.LoadMore(adapter.itemCount-1)
userBeanList: List<YueDanBean.ResultBean.PlaylistsBean>?改为 response: YueDanBean?
2.梳理结构
- 首先的YueDanFragment继承了BaseFragment与HomeView(都是三方法)
1)继承了BaseFragment复写方法 //初始绑定
a)initView()//初始化layout
b)initData()//利用p层初始化数据
c)initListener()//监听上拉与下拉滚动条,刷新与加载更多(通过presenter实现类对象)
2)继承乐单View复写方法//结合RecycleView动态更新
a) onError()
b) onloadSuccess()//上拉刷新(1c中有监听 对应recycleview的监听换了写法,可以有上下文自动推断类型)
c) loadMore()//下拉加载更多 - P层实现类
1) 调用M层网络请求的发送以及实现回调
2)回调结果中调用YuedanView接口,HomeFragment负责继承和实现
五:[Fifth Cmt]基类再次提取封装
1.观察两个Fragment的共同点
1.view
1)列表
2)下拉刷新
3)上拉加载更多
2.data
1)初始化数据
2)刷新数据
3)加载个更多数据
3.抽取 首页 乐单 定义基类将其实现界面代码拷贝进去,从基类角度分析哪些在基类保留,那些在子类实现
2.开始
基类抽取 ,还能实现显示样式的统一修改
HomeView->BaseView
Adapter->BaselistAdapter
Presenter->BaselistPresenter
base包下加了BaseListFragment类,用于所有具有下拉刷新与上拉加载更多列表的基类。把HomeFragment的代码全考过来,那个HomeView要让子类实现,用个泛型,然后loadsuccess和loadmore也用泛型
base包下加了接口BaseView<泛型>定义
a)把三个方法考过来(上拉刷新和下拉加载更多列表界面的基类)
b)将loadMore和loadSuccess内部定义该泛型类,接口不用var出来
c)原本两个接口直接把内部实现方法去掉,继承BaseView传入自己对应的bean类base包下加了接口BaseListPresenter定义
1)发现两个实现类的presenter接口内容是一样的
a.一个伴随判断标识,两个接口方法,之前写的解绑ondestory放这里面ok
b.原两个接口也不用删,继承一下就好了定义了BaseListAdapter方法(所有下拉刷新和上拉加载更多的基类)
1)子类代码考过来
a.改变adapter和holder名称
b.加泛型<>
b1)Bean类泛型
b2)adapter泛型
2)onbindviewholder有个setdata方法用于设置数据,但泛型又不知道具体方法
a.写个抽象方法,让子类传个itemview和data数据
b.create中的设定对应条目也不知道是哪个条目,写个抽象方法getitemview让子类整活(参数是一个上下文context)
3)子类adapter实现起来就贼舒服了
a.改写两个adapter继承这个,然后对应泛型初始化,复写两个抽象方法,搞定,美滋滋。
5.回到BaseListView,继承写好的BaseView,再整个泛型
a)loadSuccess自然要用到泛型,这个时候刷新就有问题了,刷新有可能是集合有可能是bean类,原本当前的泛型返回值,在updateList(getlist(response)),写getlist这一个抽象方法,让子类实现,拿到对应的list数据(地址对象转为具体参数)
6.更改原适配惰性加载的adapter,还是定义一个抽象方法获取适配器继承自BaseListadapter加个泛型取adapter,让子类实现,子类初始化柱构造器就要写出来
1)ITEMBEAN泛型
2)ITEMVIEW:View规定是View泛型
3)presenter也一样
7.改写原两个Fragment,搞定”
整着整着接口爆炸了,妈耶okhttp返回错误-460,okhttp和浏览器没问题,这。。。。百度一堆花里胡哨的都没用,最后贴吧老哥给的这个搞定了,关于网景具体故事的走这边
传送门
ps:返回有点区别,原homebean java写的,实现类返回的时候用了getresult方法,打印成功的返回值是数据,而yuedan直接返回对象
影响结果:home的getlist直接返回就行,而乐单的要返回这个对象的list数据。
.addHeader(“User-Agent”,“Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:0.9.4)”)
六:[sixth Cmt]MV适配
1:需求分析,MV和榜单有所区别,MV是有分类的,根据选择的分类切换对应的V榜,使以上层列表条目(TabLayout控件),下层页面条目(ViewPager,控制子Fragment:ViewPagerFragment)
override fun initView(): View? {val tv= TextView(context)tv.gravity= Gravity.CENTERtv.setTextColor(Color.RED)tv.text=javaClass.simpleName+name//text显示的文字为类名return tv}
- ViewPager进行匹配
1)回到MvFragment中,成功就回调 对FragmentPagerAdapter进行初始化(list,childfragmentmanager)在fragment中管理fragment
2)viewPager对应的set一个返回的fragment - TabLayout进行适配
1)MvFragment中加入success方法tabLayout.setupWithViewPager(viewPager)
上层监听,适配条目滚动fragment
2)已经实现同步滚动,下面将标题显示起来,通过c的第三个方法设置Title - 将每个界面的效果图展现出来,并且具有上拉刷新和下拉加载更多
1)要将MvPagerFragment继承具有上拉刷新和下拉加载更多的类(BaseListFragment),观察YueDanFragment,看bean类,view,adapter的需求(见2)
2)widget中创建每个条目view,xml做适配
3)实现BLF类的4个方法,
a. 创建第一个adapter,和乐单那个一
b.创建listview与实现类
c.getitem返回具体数据list
4)实现类创建request,调用netmanage,这边传参多一个可变参数,接口问题以实现选取不同列表,发送接口的sort不同,loaddate和loadmore改写一下就好了
5)改写MPagerPragment,要有数据交互了,继承(BaseListFragment)并且书写主构造器,实现三个方法,因为要做切换,init初始化拿到传过来的codesort数据,并且在实现类传入即可。
6)Widget具体条目适配一下就好了
(这个接口图片失效了)
- 接下来设置点击事件(RecycleView)
1)《第一行代码》中介绍了RV没有setonItemClickListener(),我们可以自己根据需求对子项进行监听即抛去子项点击监听,点击事件右具体的View注册
a. onCreateViewHolder中定义的holder对象,可以setonClickListener监听其内部的任意属性(例如文字或图片)
b. 监听内部复写onClick方法即可
2)BaseListAdapter实现
a. 来到BaseListFragment 里面有个getSpecialAdapter(),我们发现,所有显示条目的fragment,其adapter都是这个的子类
b.进入BaseListAdapter中,进入onBindView可以直接获取每个条目的View,(这里的holder只有一个属性view)那就给这里的itemView进行监听设置,setonClickListener()方法.
3)回到BaseListFragment
a. 复写initlistener方法,super.不能删,还要用父类实现进度条
,接着往下写,调用2)写好的函数
adapter.setMyLiserner
标准写法:
//参数里面是个函数,a是里面传递的参数,就是data,返回Unitadapter.setMyListener({a->Unit}
- 创建VedioPlayActivity作为播放MV的活动
1)建立layout,三个button,一个VideoView控件 - 修改点击内容,anko式start并且传数据(键值对)
startActivity<VideoPlayerActivity>("item" to it)
但未经序列化直接传递bean是不被允许键值对形式的,需要序列化 - 序列化bean类,建立VedioPlayBean类,继承Parcelable,
1)复写方法即可完成序列化
2)还有一个好处就是原bean类实在是tm冗杂了,这边取需要的进行 主构造器构造就好了
改写:
adapter.setMyListener {//a->Unit//it//代表传递回来的data数据 打印是Infobean类 下面类型不匹配val videoPlayBean= VideoPlayBean(it.userid,it.title,it.url)startActivity<VideoPlayerActivity>("item" to videoPlayBean)}
videoView.setOnPreparedListener{videoView.start()
七:[Seventh Cmt]MV测试,响应本地或应用外视频
ps:没事别手贱修改github的描述文件,改了push错了,百度一个多小时都不行,。。最后删库建新的库才好。。
- VideoVeiw虽然好用,但是很多特性是不支持的,比如alpha,旋转等等,引入TextureView
- 建立TextureVideoPlayerActivity,并设定 对应的layout,控件更改为TextureView,按钮也先扔掉,不然实在是太丑了,发现是全屏播放
- 关于扔掉按钮,原本VideoView直接拿到,直接start什么的都ok,还有isplaying()方法,但这个不行,他是调用mediaplayer的,得改写medioplayer。
- initdata设置监听.surfaceTextureListener通过this传递对象,并且通过当前类实现方法。
1)onSurfaceTextureSizeChanged VIEW大小改变时
2)Updated 视图更新
3)Destroyed 视图销毁 ,不实现会报错 原因是,这个有bool类型返回
a.true就不继续渲染 且声明大多数app应返回true
b. false调用release 销毁texture资源 - Available 视图可用显示界面时 但还需要解码 显示视频画面的界面
设定mideaplayer就放在这实现,而且不能在这里面定义,以前可以,现在就没用了,播放步骤:
1)设置播放源
2)设定surface源,传一个函数,参数就是我们拿到的surface,应该是绑定mediaplayer和textureView
3)异步准备
4)监听完成就令mediaplayer启动
override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {//加非空校验很重要的videoPlayBean?.let {//视图可用显示界面 但还需要解码 显示视频画面的界面// mediaPlayer.setDataSource(videoPlayBean.url)mediaPlayer.setDataSource("http://vodkgeyttp8.vod.126.net/cloudmusic/MjUyNDMxNzEw/0645927d1d174202121abf592a085c45/2b0d0faaf2c6d0feccd40b31ac668586.mp4?wsSecret=c8e0631dde1e900924976be4577b85fc&wsTime=1586933449")mediaPlayer.setSurface(Surface(surface))//设置显示画面mediaPlayer.prepareAsync()//异步准备mediaPlayer.setOnPreparedListener {mediaPlayer.start()}}}
mediaPlayer不支持硬件解码,高精也不支持,如mkv,flv,rmvb。。。。。要想做得用第三方组件
- 引入Vitamio
目的使用他们封装好,新增方法的MideaPlayer解码替换系统的,且方法保持不变
,但是呢他是继承SurfaceView(不能在滑动界面显示)
的,仍有局限,最惨的是,这家伙官网都炸了,额, 上传Github后,利用as测试,as的import module挺好用的,不过。。现在的高阶安卓根本不适用。。还有导入其他包时注意最小兼容。 - B站的ijkplayer非常好用,就用这个
1)解压,查看包结构
a.首先可以可以一些x86,x64,arm64等等这些平台依赖(生成对应平台的so库)
2)查看播放View控件-IjkVideoView,点击查看具体
a.发现他是继承FragmentLayout,神奇【视频播放就两个控件,一个是SurfaceView
还有一个是TextureViw
】,其实他就做了个包裹显示,并非真正的播放操作
b.向下找发现了IRenderView(接口)(ctrl+H)显示
b1)实现类一:SurfaceRenderView(继承SurfaceView)
b2)实现类二:TextureRenderViw(继承TextureView)
c.向下查看解码方式IMdeiaPlayer(接口)解码
c1)AndroidMediaPlayer(安卓自带)
c2)IjkMediaPlayer(自封装的)
c3)IjkExoMediaPlayer(谷歌提供数字加密解码)
3)应用仓库直接搜索依赖就好了,支持库也可以一同加进来,然后layout替换ijkvideoview,回activity修改导包
4)和之前一样,复写onDestory(),返回时videoview调用stopPlayback() - 原本的三个按钮外界,非常不美观,而且适配还非常有问题,。。所以我们再用一个框架JiaoZiVideoPlayer
1)同样的煎饼果子来一套,把prepared监听去掉,修改为自己官网提供的播放路径,鉴于网易云接口老失效,这边就引用B站-友利郁野 的钢琴,弹得非常好哦 压缩到自己土豆服务器上,测试会糊挺多,不过满足需求。点击播放,美滋滋,最后在清单文件配置横屏自动旋转,等等
- 响应本地的播放视频,安卓清单文件加入action.View接收广播系列,在原类中定义data接收数据,并取出.path具体路径,测试应用外的网络请求不需要.path直接(清单文件定义了http即可)data.tostring(),加一个if ok
- 补充videoview继承Surfaceview相当于创建新窗口 ,我们视频刷新比较高,而我们只会刷新当前窗体,并不会影响应用窗体的重新绘制,效率较高,使用MediaPlayer进行视频解码。Texture功能比较强大如旋转。
七:[Eighth Cmt]显示详情,buttongroup与viewpager双向切换绑定
- 首先修改layout,加入RadioGroup组和三个对应Radiobutton
- 双向绑定设计监听,复写initlistener方法
1)建立新的adapter:VideoPagerAdapter继承FragmentPagerAdapter,传入fm,再复写内部两个方法
a.获取Fragment
b.返回共几条
2)新建adapter对象,并监听ButtonGroup改变状态方法(setOnCheckedChangeListener),发现内部没有待实现方法,且参数只有一个直接打开设置监听
,然后写一个lambda表达式,通过buttongroup和对应i来进行viewpager的切换
3)双向绑定,所以 还要监听ViewPager(addOnPageChangeListener),发现内部有三个待实现的方法,就使用匿名内部类
,该三方法分别是
a.onPageScrollStateChanged 滑动(左右)状态 改变的回调
b.onPageScrolled 滑动的回调
c.onPageSelected 滑动状态改变 选中状态改变 做滑动下面 同步更改上面
c1)判断c中的position,并set group的check即可同步
八:[Ninth Cmt]Media媒体库与异步查询
Boat:MediaProvider
九:[Ninth Cmt]乐单条目View
Boat:AsyncQueryHandler,CursorAdapter
- 目前查询数据是放在Fragment中的,当然最好是放在Utils里岂不是美滋滋
- 做界面适配,老套路
1)条目View,Widget目录下创建VbangItemView,继承RelativeLayout
a.复写三个次构造方法
b.init方法中动态inflate中动态加载布局
2)当前结果是cursur对象,以前是bean(listview做适配继承baseadapter,定义一个list集合,装载的是每个条目的bean)转换非常麻烦,但好在安卓有一个CursorAdapter
新建adapter继承。
3)类似FragmentPagerAdapter,这个是实现两参的构造器,复写两个方法,做Vbang界面的列表适配器
a. newView() 创建条目view,绑定布局,return Widget生成的条目VBangItemView(),将我们的context参数传进去。
b.bindView() 条目view初始化与数据绑定。参数自带View,kotlin也不需要通过holder进行中间持有,其游标会转到自适应的位置,好用
b1)getview中新建了view as VbangItemView绑定,那这边新建itemview赋值转化一下。
b2)数据在cursor里,但我们内部用游标进行查数据是相对麻烦的,使用找bean类。
b3)建立AudiBean,初始化cursor查询到的四个参数主构造器
b4)创建伴生对象,内置静态方法,实现cursor到bean类的转换
c.b中分三步实现
c1)创建bean类对象
c2)判断cursor是否为空
c3)解析cursor并设置到bean对象中
4)补充:CursorAdapter是继承BaseAdapter(有四个未实现方法)CurosrAdapter已自动实现(数据类型转换与游标移动)
a.getitemid()
b.getitem()
c.getcount()
d.getview()
5 )虽然他没有适配ViewHolder(中间持有),但是kotlin可以直接拿到控件ID,所有也可以不用绑定。data是string类型,getstring()内部传递一个下标(int类型,data在那列),通过cursor的获取列下标(传入列名),其他数据同样方式拿。
audioBean.data=cursor.getString(cursor.getColumnIndex((MediaStore.Audio.Media.DATA)))
做截取的时候可以通过这种方式:
audioBean.display_name=audioBean.display_name.substring(0,audioBean.display_name.lastIndexOf("."))//截取到 最后一个.结束如:夕日坂-初音.flac
解析完成之后,返回bean类就可以了
6)adapter就可以获取对应数据
val itemBean= cursor?.let { AudioBean.getAudioBean(it) }//好用
7)对应的itemview(b1中)绑定的VbangItemView,这里setData得到的bean类数据就好
8)VbangItemView创建对应方法,是视图和数据绑定,方法内部和之前的一样
9)//text包下下的Formatter 安卓api 格式化M,G...后缀名 sizev.text=Formatter.formatFileSize(context,itemBean.size)
- 回到YuedanFragment声明,initlistener赋值adapter,并赋值给listview拿到了数据,并绑定界面,条目view(VbangItemView)中也进行了数据设置回到Fragment中进行adapter的创建解析,初始化是两个参数,第一个就当前context,第二个目前还没有(cursor)就先传个null,先不设定。
- 我们在查询这步才拿到cursor,所以呢,在Eighth Cmt中查询里的cookie设空的部分现在改为adapter ,回调的时候通过
cooike
将adapter设置对应的cursor,来到查询完成回调后
(cookie as VbangAdapter).swapCursor(cursor)先设置了数据源,再刷新
- (cookie as VbangAdapter).notifyDataSetChanged()//数据源变化时调用刷新,但adapter创建时给的是null,而我们的目的是给adapter传cursor数据,在此之前是绝对没有数据变化的。
- 补充(1),理论上这套应该ok了,但是运行炸了,原因是cursoradapater继承baseadapter中自动实现的4个方法中getitemid是主键,而我们没获取到,查询语句多加一条就好了。
- 注意
内存泄漏
问题 ,其一是Io操作没有关流,其二是cursor操作,还有就是发送广播没有反注册,handler消息没有移除等等。 - 当前我们在滑动时都要用到cursor对象,界面存在,cursor都需要,界面销毁时需要关闭cursor,fragment中复写ondestory方法,先获取adapter中的cursor设置为null
adapter?.changeCursor(null)//先自动获取到adapter中的cursor并关闭,再将传入的null替换原cursor
//内部有swapcursor帮其操作。完成生命周期 - 接下来做权限申请的优化
- 点击音乐条目跳转到播放界面
1)initListener中不仅可以设置adapter(条目变化)
2)还可以设置条目点击事件,ListView还更方便了一点,对应的adapter直接设置setOnItemClickListener
//四个参数 int是position long就是id
a.创建对应activity并设置跳转,原监听处设置点击startactivity,并传递两个参数
b.参数1:播放列表,供上下首切换 参数2:当前点击条目的position
c.获取数据集合,之前都是list与bean类,这边通过处参数i利用adapter拿到当前位置的cursor数据
d.定义一个方法audiobean中,传入当前cursor获取整个数据,存入list,具体实现,和之前那个传入cursor,获取单个的对应。
d1)创建list
d2)通过传入的cursor将游标移动到-1,因为0开始,判断cursor是否为空,let表达式;走nextitemit.moveToPosition(-1)
d3)while循环走一个,初始化bean一个,list add()一个,最后返回list就好了
回到c传入数据即可startActivity<AudioPlayerActivity>("list" to list,"position" to i)//已拿到对应参数,播放列表集合用于上下首切换
e.又是熟悉的地方,listbean类需要序列化,继承Parcelable
接口,实现各种方法,静态对象合并一下ok
3)来到AudioPlayerActivity,将传过来的数据取出
a.initdata中拿数据
a1)intent.getParcelableArrayListExtra(“list”)
a2)intent.getIntExtra(“position”,-1)//第二个参数是默认值,-1代表没有获取到,打印一下发现都是对的上的。
九:[Ninth Cmt]具体播放页面
Boat:不得不说,做适配的时候Miv4t画师的适配图实在是太棒了
- 修改布局layout,分成top ,middle,buttom三个模块
- 简单播放音乐和视频类似,获取position后 ,但这个代码在当前Activity中的话,按个返回或者切换就没了。。所以要利用安卓第二大组件:Service(其一是他优先级高,其二是被误杀后会尝试重新启动//即非软件主动销毁,与后面空指针异常的联系,不过高办好、本好像更了默认,不会重新启动),与绑定上一曲,下一曲
override fun initDate() {val list=intent.getParcelableArrayListExtra<AudioBean>("list")val position=intent.getIntExtra("position",-1)//-1代表没有获取到//println("111111111111list=$list 2222222222positin=$position") 队列与位置正确//播放音乐//1.创建进入idle状态val mediaPlayer=MediaPlayer()//2.设置播放路径mediaPlayer.setDataSource(list.get(position).data)//3.异步准备mediaPlayer.prepareAsync()//3.监听准备mediaPlayer.setOnPreparedListener{mediaPlayer.start()}}
ps服务类太吓人了,手机的清除内存对他根本没用,活动管了仍然在运行服务,点击其他歌曲后两首歌会一起放,所以安卓软件光靠一键加速是没用的,得重启
//先开启startService(intent)//再绑定bindService(intent,conn, Context.BIND_AUTO_CREATE)//第二个参数是接口connection,第三个参数是绑定后若未创建则会自动创建出来
c4)intent优化,原本上一级拿到两个参数测试,这一级封装intent转发,那c1和c2直接省略=getintent就好了。
var intent=intent//这是getintent 和1中一样取从VBangFragment中的数据//修改intent.setClass(this,AudioService::class.java)
override fun initDate() {//println("1111 positin=$position")var intent=intent//这是getintent 和1中一样取从VBangFragment中的数据//修改目标走向intent.setClass(this, AudioService::class.java)bindService(intent,conn, Context.BIND_AUTO_CREATE)//第二个参数是接口connection//回到AudioService进行播放startService(intent)}
十:[Tenth Cmt]播放页面适配与动画效果
Boat:缺陷待完,首先堆叠多首一起播放,其次是只能点击一次进入播放页
1. 处理点击播放和暂停的切换
- 来到AudioPlayerActivity
1)复写initlistener,监听按钮必不可少,先设置state按钮点击监听,并且用传this的方法,让当前Activity实现onClick接口,原因是目前播放暂停可以写里面,但后期还有上下一曲,播放状态等等,里面用when表达式看id,更加便捷目前只有state按钮控制播放状态
2)定义updatePlayState方法,供when(点击state播放暂停图标)调用,具体操作如下
a. 更新播放状态(操纵Service中的mediaplayer->在service中实现绑定服务返回的service接口,t通过九当中new的一个Iservice接口的绑定对象.
b. 我们拿到对象但是没方法,于是原接口中写一个就好了,updatePlayState(),好了这个对象调用,我们发现Service报错了
c. 原因是我们自己的AudioBinder是继承iService绑定的,这边要继承所有接口,继承一下,ok
d. 在Service中实现
d1)获取当前播放状态,这里再定义一个方法isPlaying(),方便以后复用,就直接eturn mediaPlayer?.isPlaying
bool类型即可
d2)切换播放状态,返回值非空,后进行判断,1就说明点了暂停,执行pause(),0就执行start()
3)改变播放状态的图标updatePlayStateBtn(),供复用,而且和改变播放实现代码的对应,组个cp?
a. 方法内呢无参,还是我们的iService先调用isPlaying(),根据状态改成相应的即可
2. 界面音乐标题,动画以及进度的更新
- 业务逻辑在播放音乐之后,我们AudioBinder类里的监听onprepared方法才是完成播放的,medaiplayerhou,写个方法通知界面更新notifyUpdateUi()
1)在SerVice通知Activity中更新
,之前是Activity通过iservice绑定主动到service中拿东西,而现在是service要主动发信号给activity,实现方法
a. Activity中定义一个接口,用iservice传到service,然后再通过这个接口将数据回传,额。。有点麻烦
b. 通过handler,Activity中定义一个handler,再通过handler与iService将handler传给service,开始播放时通过handler传递消息就中了。a和b耦合性较高,Service要持有Activity一个变量的引用,可能会内存泄漏,即销毁某个时,变量还存在
c. 广播方式。Activity中定义广播接收者,Service那个方法中发送广播,接收即更新,退出关闭广播接收者,岂不是美滋滋
d. EventBus应用类广播(导个jar包,也能搜到)4个特性美滋滋
2)首先Acitvity中初始化initData注册一下EventBus.getDefault().register(this)this指当前class,和广播一样,销毁时反注册一下EventBus.getDefault().unregister(this)
a. Activity中定义接收器onEvent,方法名有4种模式
a1)PostThread:啥线程中发送,接收端啥线程中执行发送
a2)MainThread:主线程执行
a3)Background:后台队列依次执行
a4)Async:后台并行执行
b. 由于数据比较简单,而且会操作界面UI数据,所以直接主线程了fun onEventMainThread(itembean:AudioBean)指定接收的Bean类
3)回到1)中方法写EventBus.getDefault().post(position?.let { list?.get(it) }) list里面装的是bean类,通过position拿到对应的bean对象。 - 怎么实现的呢,我们initdata中注册了bus,然后会扫描当前类中的onEvent方法,并进行保存,我们Service中传bean类,就会在保存的onEvent方法中找到参数的bean类的对应方法就OK了美滋滋
- 在bus接收器中取出来 设置一下就好了,值得一提的是动画设置,再次调用方法改变播放/暂停显示按钮。
1)首先动画其实是多张图片交替animation-list动画帧与内部的ltem图片组,和botton图片组
2)更新播放状态后开启动画切换
drawable = audio_anim.drawable as AnimationDrawablesrc方式拿到并转化为动画类型的drawable,之后**drawable.start()**即可ps:若非src而是background方式设置背景 则拿到应该:audio_anim.getbackground,暂停的话调用stop方法即可
3)drawable扔外边去,再实现一下动画的同步暂停,咱们有两个方法,一更新播放状态,和更新图标状态,那果断在图标状态更新,两个ss一加,搞定。 - initlistener再把返回按钮加个click侦测,finish()搞定
- 实现进度的显示,其一是实时变化的进度,其二是总进度
1)总之得先拿到总时长数据
a. 通过我们绑定的iService对象写个getduration()方法获取(iservice来个抽象方法,自己的service对应binder实现return mediaplayer。duration即可),记录一下
b. 不过这里获取到方法是毫秒,嗯,待会写个方法转一下就好了,Utils包下建立StringUtil静态类,写个方法一转。
result=String.format("%02d:%02d",min,sec)//不足两位以0补齐这里注意一下就好了。
2.1)随后要获取当前进度,嗯,写个方法,和总时长类似startUpadateProgress(),先同样写一个方法getprogress()返回mediaPlayer.currentPosition,这里同样是毫秒
2.2) 更新进度数据
a. 虽然是对界面进行改变,由于和前面的方法没啥交集,主要是写里面太冗杂了,再写个方法拉倒 updateProgress(),里面传的是获取到的progress
b. 对应settext搞定
2.3)定时获取进度,ntime更新啊,方法其一呢就是一直获取,然后设置进程睡眠 ,这个呢发证法。其二呢,安卓handler也能做到
a. 定义个handler,再讲EventBus提到过handler,这里就可以用了,发送一个延时空消息 handler.sendEmptyMessageDelayed(MSG_PROGRESS,1000)//发送空消息并有一定的延时,第一个参数是标志变量,自己外层定义一下
b. 定义handler用匿名内部类方式打开,复写接收方法handleMessage(msg:Message),然后when等于咱标志量,相等再执行**startUpadateProgress()**方法,完成循环调用
3)回到2.2)中,将毫秒传过去之后,搞定
progress.text = StringUtil.parseDuration(pro)+"/"+ StringUtil.parseDuration(duration)
十一:[Eleventh Commit]播放列表与上下一曲
处理进度条同步
1)在onEventMainThread()即EventBus中获取总进度后,通过max属性就可以给咱进度条设置最大值
2)我们实时改变进度时间的方法中updateProgress()通setprogress()就好了
3)点击暂停后停止进度条滚动
a. 这个和放动画的类似(只不过是将操作变为handler消息(咱定义的标志))移除和添加即可,在相同位置处理内存泄漏问题,发送延时消息传一个message,其中有target handler作为类指针引用,若退出界面,消息任在消息队列中,handler也任然持有,咱们是定义在activity中的,对应这个AC也无法销毁。解决方法两种:
1)handler放静态伴生对象里,但是呢由于匿名类里有调用外部方法,其他方法也要改为静态,比较麻烦
2)退出界面,清空handler消息即可,两个都可以走正常的销毁流程,ondestory方法中
handler.removeCallbacksAndMessages(null) 内部一个token参数,null就是全清空进度条交互
1)首先当然要给进度条设置一个箭头,和back那些一样,来到initlistener
progress_sk.setOnSeekBarChangeListener() 发现内部参数是对应listener还有三个未实现方法,不能打开解决方法其一是内部类,其二是传this,让当前活动对象来实现,和之前state对象一样,是不是有内味了,总共三个方法:
a.onProgressChanged(),其中参数进度一改变就回调 改变后进度和ture//用户手指改变 false代码式改变
b.onStartTrackingTouch() 触碰seekbar的回调
c.onStopTrackingTouch() 手指离开seekbar回调
2)我们要监听进度条的拖动改变,就是在a方法中进行,分三个阶段
a. 侦测用户操作,如果是代码使进度条变化就直接return
b. 更新播放进度,通过我们的Service绑定,将我们的progress参数传递过去,任然是接口加上静态方法,再通过Service里自己定义的Binder类定义seekTo(),通过mediaPlayer实现一下传参,音乐就能调进度了,最后将当前进程数字同步,调用updateProgress就好了设置自动播放下一首
1)基于算法上就是当现在的时间等于总时长就切歌,但是安卓的mediaplayer贴心的提功力setOnCompletionListener方法,咱传this用类实现回调onCompletion,再封装这个方法autoPlayNext(),里面用when根据模式切歌就行,随机调用Random(),这里kotlin的random报错了,于是改用javarandom,更改position,最后调用playItem()即可。切换播放模式,现在APATY的initlistener的mode控件设置点击事件,在之前写好的click回调的when中同样写个方法updatePlayMode()
1)修改service中的逻辑模式
a. service操作和之前一样,接口写个方法updatePlayMode(),iService调用,Service写具体的逻辑模块
b.传入当前模式,when写一下all,singer,random三模式的相互转换
2)修改界面模式图标
a.定义个方法据当前播放模式updatePlayModeBtn() 修改图标,其中也分两步
a1)获取播放模式,仍然是写个方法,通过service实现getPlayMode ,操作也很简单,到了service就可以直接return mode了。回到activity,报红,做个为空判断,iService为空就不管他了。
a2)设置对应图标,分三种情况按拿到的写图标就行了,这里有个问题,service中的三状态拿不到,把那三个改为静态对象,搞定。播放模式状态不保存,未同步service中的模式
1)打开service会执行playitem,会有通知更新的方法,在eventbus说过,回调更新界面也可以更新播放模式,方法加上去,尝试了一下,可行。关闭之后就不行了,内存中服务都销毁了。
2)保存到本地,方法1:文件式保存,方法2:sharepreference保存
a.定义名称与私有路径,在最外层惰性加载一下,
** val sp by lazy{getSharedPreferences(“config”,Context.MODE_PRIVATE)}**
b.保存播放模式,UpdatePlayMod中负责切换,切换后保存**sp.edit().putInt(“mode”,mode).commit()**键值对
c.打开更新,oncreate方法
mode=sp.getInt(“mode”,1) 如果拿不到播放模式就默认为1模式即可。
7. 设置上一曲和下一曲,同样设置监听,写方法,iService实现,这里留了个坑,随机的上一曲是没有记录的,上下一曲都是随机如果media已经存在就先释放掉,不然new对象会堆积,这里我也不太理解,我只定义了一个变量,居然不会被覆盖掉,在playItem做重置,和之前的videoview类似
点击继续播发现重新开始了,onStartCommand()中加入判断思想,如果不是同一首再设置获取和播放,(返回Activity销毁但service不会销毁,但是界面都没了)bind方法里有play和更新界面notifyUI,主动通知即可,但进度和数值有问题,原因是初始化时混合启动,绑定开启顺序都可以,但这里有问题,应该先绑定在开启,是因为没拿到iservice接口就更新的缘故,先绑定就能拿到binder
做完了8可能会报错,原因是刚开始点第一个下标也是0,咱初始化也是0。。会认为相等就不再执行获取list,所以后面取就会报空指针异常,修改默认值为-2或者null
通过popwindow显示播放列表(理想效果和网易云一样最好),同样的先定义一个监听,方法,处理onclick
1)界面的显示,所以写个activity的方法 showPlayList(),这个弹出界面使用的是popwindo
2) Widget包下创建popwindow,继承popwindow窗体,复写init方法
a1)设置布局
找到对应的界面context,由于是控件找不到,使用主构造器传一下,写个布局R.layout.pop_playlist,里面是一个listview
a2)设置好之后设置contentView展示出来
b.代码中设置宽度和高度(不设置显示不出来),通过windowmanager和point()
3)回到1的方法中,新建主构造器对象,并且传入当前context,showAsDropDown()方法是设定弹出左下方到当前参数view的左上方,要想移到底下,设定y偏移量为参考控件的高度即可。
4)回到widget中设定焦点(isFocusable)和外部点击(isOutsideTouchable都为true),实现设置setBackgroundDrawable(ColorDrawable())能够响应返回按钮(适配低版本点击返回按钮里面写了适配dismiss)
十一:[Twelvth Commit]
- 适配播放列表,和以前一样
1)widget下适配条目View,并写对应布局ps:设置最大一行很重要
2)来到定义的PopWindow中,init方法里获取listview,由于只是控件,(非fragment、activity)不能直接拿到id,得findviewbyid
3)做adapter适配,复写4个方法,注意getView的操作,其中kotlin形参不可修改,还有convertView缓存机制,这边定义了条目PopListItemView就不需要viewholder了。返回前对应的itemView选择setData()在我们的桃木中实现。怎么拿到adapter呢,让外部主构造器传递即可
4)来到AudioPlayerActivity方法中, showPlayList()展示播放列表定义一个对应的adapter,数据需要列表集合,写个抽象方法,让Service整,返回list就管,注意这边改写了空判断 - 点击播放列表条目就播放歌曲
1)在设置adapter后设置条目点击事件,listView的sOCL,方法内需要一个listener对象来执行回调
2)乐单Activity中,创建PlayListPopWindow处修改主构造器,传this,让当前类实现回调
3)和之前一样,通过iService写个方法用position传递,实现很有意思,用了注释
this@AudioService.position=position//注解
this注解,原本是实现的bean类,现在指向到AudioService - 设置弹入/出动画
animationStyle =R.style.pop
指定文件,添加进入和退出动画
1)进入设置translate平移,设定y方向平移和时间1s
2)退出弄成相反就好 - 改变弹出背景颜色,播放列表弹出popwindow是新窗体覆盖在应用程序窗体上,弹出修改应用程序透明度即可
1)这是界面的操作,来到activity,showplaylist方法中通过showAsDropDown显示,来到对应PlayListPopWindow组件复写这个方法,super不要去掉,再复写dismiss,具体的动画操作放这里面。
2)主构造器拿到window,activity传window,拿到Ac的窗口,获取不透明度,分别两个方法设置一下就好了,ps,复写方法里要拿到参数不能放init里面 - 增设自定义通知
- 回到FarawayPlayer,显示通知栏
1)在Service中onPrepared()准备完成后,通知界面更新后也要通知显示“通知栏”,先将6中的show方法及相关核心代码拷贝过来
2)getNotification()获取具体Notification对象时,生成的对象报错了NotificationCompat.Builder(this),原因是这里的this指binder对象(在这里面),应指定为Service,参数改为this@AudioService用了kotlin中的注解
3)service中是有list数据和position的,所以标题什么的直接拿出来就好了
4)延时加载的点击事件中,这里的Intent操作,service,不需要操作界面,而其中的this也要指向service【后面也改为对应的getService即可】获取Service操作就好了,不用显示界面,只有点击主体才跳转到播放界面 - 设置通知栏点击事件,点击通知栏主体跳转到播放界面
1)这里崩了,原因是空指针异常,原intent传递list现在拿不到了
a. 进入Activity方法中会先执行initdate()方法,再通过绑定和开启服务(传递intent(VbangFragment中)),而点击主体创建的intent是不一样的,会再次执行onStartCommand,咱们数据是从intent获取的,此时当然获取不到,返回-2,list也获取不到,所以报null【即进入AC后仍然会进入Service,但这个intent获取不到数据】
2)那就好办了,每个intent都传了from标志,OSC方法加个when判断,非vbang的就不获取和播放,但刷新界面,原本的显示条目都没了,那好整,和再次点击条目一样,用绑定的binder,执行更新UI方法就好了。
3)新问题:点击条进入的是栈结构,单片直退,多片重叠,解决单片直退问题呢,先启动主界面,再启动播放界面,所以启动改为Activitys,结合数组
4)但播放界面APAcitivty会堆叠多次,应设定SimpleTask启动模式(见第一行代码)
private fun getPendingIntent(): PendingIntent? {val intentM = Intent(this@AudioService, MainActivity::class.java)//点击主体进入当前界面中val intentA = Intent(this@AudioService,AudioPlayerActivity::class.java)//点击主体进入当前界面中intentA.putExtra("from",FROM_CONTENT)val intents= arrayOf(intentM,intentA)val pendingIntent = PendingIntent.getActivities(this@AudioService,1,intents, PendingIntent.FLAG_UPDATE_CURRENT)return pendingIntent}
- 通知栏点击事件,点击上下一曲以及暂停的切换
1)[7.2]中when判断把其他的代码补全,由于写过方法,通过binder调用即可,美滋滋,但是呢暂停涉及图标变化,虽然功能实现了,但界面和通知栏的图标都还没变【原因就是:
a. 原本的AC中点击state,调用service的updatePlayState()和AC的界面更新
b. 在servie中的通知栏只能完成播放暂停核心代码,无法干涉界面】
c.我们之前更新状态通过EventBus,是不是有内味了,这个消息发过去,AC会自动根据播放状态进界面更新,但此时我们还需要更新通知栏的状态
2)更新通知图标有点小麻烦,因为不是在同一个线程里面
a.找到显示通知栏的showNotification()方法,将manager显现的第二个参数getnotification对象具现出来,和他的manager一样,放到最外面service层,这样更新通知栏的图标都那到对象了,岂不是手到擒来
3)显示是通过咱点击播放start和pause方法后notification?.contentView?.setImageViewResource()
设定对应图片
4)最后一步通过manager重新notify当前id即可。要想没提示音设置低等级即可
安卓8.0,往后要设置channelid测试了一下,setsmallicon有些机型没用,而原image最好使用drawble中的才管用。
十二:[Thirteenth Commit]静态多行歌词【绘制画面与切换】
1. 需求【重点是求每行的x和y】
- 歌词拖动,滚动,播放到哪一行,哪一行居中
- 拖动更新较难,实现滚动也较难
- 先做播放到哪行,哪行居中 ,绘制多行居中文本
- 先绘制居中,再绘制多行文本,再层级向上完成
2.绘制单行居中文本
- 歌词是自定义View,不能用单纯的TextView实现,自定义两种
1)自定义组合View
2)继承View measure layout draw - 这里选择继承View绘制一下内容就好了
1)Widget下建立 LyricView继承View,复写三个次级构造方法
2)查看View绘制流程【View 画布 画笔】
a. measure方法进行测量
b. layout方法进行布局
c. draw方法进行绘制,其中有6个步骤,View调第三步ondraw方法绘制主体,ViewGroup将孩子绘制出来
3)复写ondraw方法,参数是一个画布canvas
a. 定义显示的文字,通过canvas的.drawText方法,传四个参数,前两个是坐标,第三个是定义的文字,第四是个是画笔,(0,0)任然是左上角那个店,显示文本的左下角定位在这个点上
b. 声明抗锯齿画笔val paint by lazy{Paint(Paint.ANTI_ALIAS_FLAG)}
- 找到middle其中引入的歌词布局,改为我们刚写的LyricView控件,因为咱们是继承了View,发现就是在左上角
- 根据公式要拿到四个参数
1)复写onSizeChanged() 这是draw第二个方法执行结束后layout布局完后执行布局调用,可以拿到View的x,y参数
2)歌词是绘制的,通过画笔拿到x,y Rect矩形类
val bounds= Rect()//新建bounds矩形对象,确定长/宽先整形状paint.getTextBounds(text,0,text.length,bounds)//传递指针,参数指向boundsval textW=bounds .width()val textH=bounds.height()val x=viewW/2-textW/2val y=viewH/2-textH/2
- 增加字体大小与颜色,变量声明,init中进行赋值,ps用到的字体定义到R.dimen包下,定义的颜色到R.color下
在inDraw中,设置paint的textSize和color就可以看到效果代码中用到颜色都要先定义出来,要不然可能会认为是未定义的变量
init {bigSize=resources.getDimension(R.dimen.bigSize)smallSize=resources.getDimension(R.dimen.smallSize)white=resources.getColor(R.color.white)green=resources.getColor(R.color.green)}
- 进一步优化,在init中设置画笔
paint.textAlign=Paint.Align.CENTER//在x方向确定位置是以中间确定位置
,再来到drawText方法设置为view的一半即可
3.绘制多行居中文本【静态】
- 定义歌词的bean类,先看这张图,时间+内容 建立bean类(网易云的歌词就比较复杂)
- 具体步骤
1)显示多行,行高思路
2)根据图示,我们首先要拿到居中行
a. 在ondraw方法中抽取drawMutieLine(canvas)用于多行的显示
b. init中的定义for循环存入list类,20行,
c. 定义centerLine居中行初始为10,拿到对应的数据val centerText=list.get(centerLine).content//拿到居中行文本
d. 确定位置,和单行的操作类似
3)紧接着for循环通过index遍历绘制内容, 拿到当前行的x和y(通过公式)并取出对应的内容,按公式里乘上行高,我们定义一下,再init中lineHeight = resources.getDimensionPixelOffset(R.dimen.lineHeight)
和颜色一样,类css定义dimen一下
4)最后和以前一样,通过canvas绘制出来就好了 - 优化
1)index取出判断,中心行变为大绿,其他几行也是有绘制的,超出了高度就不显示了
2)优化,如果超出位置就不再绘制了curY这个当前位置就很管用了。
a. 代码
b. 分离,如果大于底部就直接跳出,因为目前后面的不会再显示
if(curY<0 || curY>viewH+lineHeight)continue
十三:[fourteenth Commit]静态多行歌词【绘制画面与切换】
1. 播放到哪行让哪行居中[播放进度相关]
- 写个方法updateProgress(),psAATY中也有这个同名方法,对应绘制,和之前一样,重点找居中行
1)首先得获取居中行行号[通过list的starttime和progress]
a. 进度大于最后一行的开始时间,则最后一行居中
b. 其他行居中的话 循环遍历集合 找到居中行
2)找到居中行后,绘制多行歌词 重新调用ondraw方法,其中有三种无参方法
a. invalidate()执行ondraw,宽度变化不管 只会改变内容,条目控件的绘
b. postInvalidate() 执行ondraw方法,可在子线程刷新
c. requestLayout 执行ondraw,view布局(宽度和高度)参数个改变时。刷新,view宽高重新绘制 - 来到AATY的同名方法将其传过来即可
- 小结:设置进度,找到居中行,重新执行ondraw刷新
2.首先歌词滚动过渡
- 首先得知道走的距离及时间,求居中行的偏移距离y,其他行按这个一起偏移
1)偏移时间指:居中行开始时间到真正开始走的时间。
a. progress-开始时间,即指正在偏移的时间
2)行可用时间指当前行开始居中到下一行居中的时间
a. 最后一行居中,行可用即为doration总时长-最后一行开始
b. 其他行即为下一行开始时间(nexttime)-居中开始时间 - 来到多行移动方法drawMutieLine()内,走流程,储存总时长和进度,然后走流程,最后回到APATY,拿到总进度的地方就设置过来
- 发现好像有点卡,不够丝滑?没事,调整通过handlr定时获取进度(startUpadateProgress)的频率,或者直接去掉delayed,收到消息就更新,丝滑。
- 查看歌词文件,我的虚拟机里暂时看不了emmm
- 网易云本地的歌词到是有,不过他是通过id来对应歌的,并且他是bean类,没个还不一样==领会精神就好了
十四:歌词解析
1.创建解析Util类LyricUtil,解析歌词文件获取歌词集合
- 创建方法parseLyric(),传递一个文件,返回bean类list
1)创建集合保存bean类
2)判断是否为空,做放误
3)存在就通过IO流读取
a. java的BufferedReader方式
b. kotlin式可以直接拿到file
var bfr=BufferedReader(InputStreamReader(FileInputStream(file),"gbk"))//返回集合var line=bfr.readLine()while (line!=null){//解析line=bfr.readLine()}return list
var linesList=file.readLines(Charset.forName("gbk"))//读取歌曲文件,返回每行撒数据的集合for (line in linesList){//解析一行 通过集合表示val lineList:List<LyricBean> =parseLine(line)//添加到集合中list.addAll(lineList)}
其中的parseLine是一个解析方法【原因是那种歌词文本会有两个bean类,即重复语合并如:[01:33.67 [02:46.87 伤心的泪儿谁来擦 有些会这样,重】
1)使用split进行分割成三个集合,如果是time,time,content格式,根据timefor循环添加,最后全拿到后通过sortBy 方法合并升序拍了
2)ps:时间我们定义的是int,这边拿到数据转换再add进list通过substring(1)去掉第一个括号,小时,分钟,秒数和之前一样,按:切割,切割就没了,list存起来,看size上个方法中要传一个歌词文件,这里得拿到文件,定义Util静态类专门拿到文件,参数是名称,返回位置,获取外部存储,指定路径
var dir=File(Environment.getExternalStorageDirectory(),"Doload/Lyric")fun loadLyricFile(display_name:String):File{return File(dir,display_name+".lrc")}
- 回到LyricView,获取当前播放名称,将歌词文件添加进集合
1)写个方法setSomeName,传个名字过来,同那个获取总进度的
2)内部依次通过刚刚写的LyricLoader和LyricUtil传递歌名和文件,就可以拿到歌词集合,addall - 来到AC的onEvent方法中设置歌名即可,这边可能会乱码,指定下编码格式,在Util中Readline设置gbk编码格式
- 软件崩溃说越界了,自定义View显示,进入界面,歌词list为设置,未播放时就会对界面歌词View初始化ondraw方法,之前我们直接写死初始化的
1)将centerline变为0,因为有些拿不到歌词
2)onDraw(canvas)加个判断list.size==0,则调用之前的加载单行,其他的加载多行
a. 未准备好
b. 拿不到歌词 - 后续优化,View中解析是IO操作,可以放在异步线程,java是用thread,Kotlin调用doAsync,由于换线程,this加上注释@LyricView指定一下就行了。
2. 手势调节歌词【无法加载歌词,学习精神】
- 在LyricView中复写onTouchEvent方法,返回一个true
1)手指按下时,停止通过进度更新歌词
a. MotionEvent.ACTION_Down:(鼠标按下),停止通过进度更新歌词,不执行updateprogress(传递progress)代码即可,加个flag变量,updateByPro=true,按下变为false
b. ACTION_UP手指松开,flag=true
2)手指拖动歌词,所有歌词偏移,得求手指的move,手指按下a中记录y值
c. ACTION_MOVE,记录当前的y值,偏移y等于当前-记录,将歌词重新绘制出来,一说到这个,肯定用到onDraw(),还记得drawMutieLine(),这边如果标志变量为false,这边的偏移量也不用了(加个if),说明是手指控制,偏移y值加到外面,如果手势更新设置算的偏移y给咱的偏移对象,重新绘制执行invidate
3)新问题,拖动差值,加上的话会有偏差,因为任然在放歌,把原本进度的y记录,最后手势更新一起加上绘制就可以了。目前只是绘制出来
十五:拖动歌词更新
- 处理居中行的变化,每个都有行高,移动最终的偏移量大于原播放行高就要变化,偏移行数整除就知道了,然后一加搞定。
- 发现歌词移动速度比我们手移得快,偏移位置的问题,重新确定偏移y值,去掉整行高 。还有一个原因是居中行的变化,第几个居中,偏移几个像素,这里不细说了,暂时没法实践
- 拖动同步更新,居中行改变时,更新播放进度
1)java用接口回调,kotlin函数一等公民写个进度回调函数和设置进度回调函数
//进度回调函数private var listener:((progress:Int)->Unit)? = null//设置进度回调函数函数fun setProgressListener(listener:(progress:Int)->Unit){this.listener = listener}
2)LyricView中调用更新播放进度,传递当前行的时间,之前做的移动行得到当前时间
3)APATY中设置一个拖动监听,lyricView.setProgresslistener,it接收并更新播放进度及显示,用之前的方法就可以了(seekto和updatePregress)
十六:优化更新
1.歌曲信息
- 歌曲名的显示问题,如:园游会显示为周杰伦-园游会,数据库查询问题,以前LocalPlayer的方法查询数据库,将DISPLAY_NAME改为Title
1)VBangFragment在查询的部分修改
2)在AudioBean设置参数的地方修改,变为Title后就不用截取.flac了
2.通知栏
- 设置对应的歌曲封面,原本是通过找到的id来进行对应封面库匹配,结果存入到bean类中的Bitmap对象,基本属性列举
- 补充一下,访问本地媒体库的播放时长,mediaplayer是有自己的方法的,不用在查询时做。 而且可以加一个favourite是否喜欢的属性
- Drawable和bitmap转换,关于通知栏,效果是这样的,以后封装成专辑封面
【Kotlin】FarawayPlayer相关推荐
- Kotlin第5篇项目实战2:开发【Kotlin】版QQ2006聊天工具-关东升-专题视频课程
Kotlin第5篇项目实战2:开发[Kotlin]版QQ2006聊天工具-420人已学习 课程介绍 本视频是智捷课堂推出的一套"Kotlin语言学习立体教程"的视频 ...
- Kotlin第4篇 【Kotlin】进阶视频课程-关东升-专题视频课程
Kotlin第4篇 [Kotlin]进阶视频课程-376人已学习 课程介绍 本视频是智捷课堂推出的一套"Kotlin语言学习立体教程"的视频第四部分,主要内容包括: ...
- Kotlin从小白到大牛第1篇 【Kotlin】基础视频课程-关东升-专题视频课程
Kotlin从小白到大牛第1篇 [Kotlin]基础视频课程-7239人已学习 课程介绍 本视频是智捷课堂推出的一套"Kotlin语言学习立体教程"的视频第一部分, ...
- 【Kotlin】Kotlin 自定义组件 ( 自定义 View | 自定义 SurfaceView )
文章目录 一.自定义 View 组件 ( Kotlin ) 二.自定义 SurfaceView 组件 ( Kotlin ) 自定义组件构造函数统一在 constructor(context: Cont ...
- 【Kotlin】循环控制流 ( for 循环 | Iterator 遍历形式 | Iterator 遍历要求 | IntArray 源码解析 )
文章目录 一.For 循环 二.For 循环遍历 Iterator 对象 三.Iterator 遍历要求 四.IntArray 源码解析 一.For 循环 For 循环有两种遍历形式 : 一种是 遍历 ...
- 【Kotlin】函数类型 ( 函数类型 | 带参数名称的参数列表 | 可空函数类型 | 复杂函数类型 | 带接收者函数类型 | 函数类型别名 | 函数类型实例化 | 函数调用 )
文章目录 I . 函数类型 II . 带参数名的参数列表 III . 可空函数类型 IV . 复杂函数类型解读 V . 函数类型别名 VI . 带 接收者类型 的函数类型 VII . 函数类型实例化 ...
- 【Kotlin】Lambda 表达式 ( 简介 | 表达式语法 | 表达式类型 | 表达式返回值 | 调用方式 | 完整示例 )
文章目录 I . Lambda 表达式 简介 II . Lambda 表达式语法 III . Lambda 表达式类型 IV . Lambda 表达式返回值 V . Lambda 表达式调用 VI . ...
- 【Kotlin】变量简介 ( 可空类型 | lateinit | 初始化判定 | 非空类型 | !! 与 ? 修饰符 | ?= ?. ?: 运算符 | 抽象属性变量)
文章目录 I . Kotlin 变量总结 II . Kotlin 非空变量 III . 非空变量不能赋空 IV . lateinit 关键字 V . lateinit 初始化判定 VI . Kotli ...
- 【Kotlin】Kotlin 中使用 ButterKnife ( 仅用于适配 Kotlin 语言 | 不推荐新项目使用 )
文章目录 I . 特别注意 : ButterKnife 已停止维护 ( 新项目禁止使用该框架 ) II . Android Studio 中配置 Kotlin 和 ButterKnife 步骤 III ...
- 【Kotlin】扩展接收者 与 分发接收者 ( 类内部扩展用法 | 注意事项 | open 修饰扩展 )
文章目录 I . 类内部扩展其它类 II . 扩展接收者 与 分发接收者 注意事项 III . open 修饰 分发接收者 类型中的扩展 I . 类内部扩展其它类 1 . 扩展函数 / 属性声明的位置 ...
最新文章
- 剑指offer:把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
- 很好的阻止了事件的发生_请定好您的闹钟,八月,夜空中将发生这13件超酷的天文事件...
- 技嘉主板万能网卡驱动_技嘉Z490系列主板来袭:16相供电/钽电容,堆料更进一步...
- POJ - 2352 Stars(线段树/树状数组)
- 牛客 216 C 小K的疑惑
- 扩容是元素还是数组_Java中对数组的操作
- Flask+uwsgi+Nginx环境搭建
- [转载] Java8新特新--Stream语法应用在ArrayList的元素移除和排序
- SQL SERVER LEFT JOIN, INNER JOIN, RIGHT JOIN
- 【c++leetcode】翻转链表
- [渝粤教育] 中国地质大学 中外美术史 复习题
- 24_多易教育之《yiee数据运营系统》OLAP平台-运营分析篇
- mysql字符串查询_mysql字符串查询常用命令
- 利用MapGis6.7 对 jpg图像文件进行图形校准
- 结构专业规范大全_监理签字用语规范大全,就是这么专业!
- 工具网站:10个国外免费、无版权、高清图片素材站
- java读取pdf文档
- 【五校联考5day2】光棍
- 【WebService】Caused by: org.apache.cxf.transport.http.HTTPException: HTTP respon ...
- 抖音、快手、B站的广告投放原理
热门文章
- 新股高中签率的技巧|提高新股中签率技巧
- 2021年12月22日 腾讯会议Ipad录屏无法录制声音问题解决
- pkg-config
- Oracle EBS AutoConfig详解
- 工业制造中的UWB定位技术
- GD32 定时器+一个IO实现SIF读取
- 荣耀8X0安卓系统切换鸿蒙系统,华为大改安卓Q,EMUI 10将启用全新UI,花粉沸腾:静候鸿蒙...
- Qt 实现画线笔锋效果详细原理
- 路由器、交换机设备管理
- System.Data.SqlClient.SqlError: Exclusive access could not be obtained because the database is in us