Android Compose——一个简单的Bilibili APP

这篇具有很好参考价值的文章主要介绍了Android Compose——一个简单的Bilibili APP。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

简介

此Demo采用Android Compose声明式UI编写而成,主体采用MVVM设计框架,Demo涉及到的主要技术包括:Flow、Coroutines、Retrofit、Okhttp、Hilt以及适配了深色模式等;主要数据来源于Bilibili API。

依赖

Demo中所使用的依赖如下表格所示

库名称 备注
Flow
Coroutines 协程
Retrofit 网络
Okhttp 网络
Hilt 依赖注入
room 数据存储
coil 异步加载图片
paging 分页加载
media3-exoplayer 视频

效果

登录

登录在Demo中分为WebView嵌入B站网页实现获取Cookie和自主实现登录,由于后者需要通过极验API验证,所以暂且采用前者获取Cookie,后者绘制了基本view和基本逻辑

效果

Android Compose——一个简单的Bilibili APPAndroid Compose——一个简单的Bilibili APP

WebView

由于登录暂未实现,故而此处就介绍使用WebView获取Cookie。由于在Compose中并未直接提供WebView组件,故使用AndroidView进行引入。以下代码对WebView进行了一个简单的封装,我们只需要在onPageFinished方法中回调所获的cookie即可,然后保存到缓存文件即可

@Composable
fun CustomWebView(
    modifier: Modifier = Modifier,
    url:String,
    onBack: (webView: WebView?) -> Unit,
    onProgressChange: (progress:Int)->Unit = {},
    initSettings: (webSettings: WebSettings?) -> Unit = {},
    onReceivedError: (error: WebResourceError?) -> Unit = {},
    onCookie:(String)->Unit = {}
){
    val webViewChromeClient = object: WebChromeClient(){
        override fun onProgressChanged(view: WebView?, newProgress: Int) {
            //回调网页内容加载进度
            onProgressChange(newProgress)
            super.onProgressChanged(view, newProgress)
        }
    }
    val webViewClient = object: WebViewClient(){
        override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
            super.onPageStarted(view, url, favicon)
            onProgressChange(-1)
        }
        override fun onPageFinished(view: WebView?, url: String?) {
            super.onPageFinished(view, url)
            onProgressChange(100)
            //监听获取cookie
            val cookie = CookieManager.getInstance().getCookie(url)
            cookie?.let{ onCookie(cookie) }
        }
        override fun shouldOverrideUrlLoading(
            view: WebView?,
            request: WebResourceRequest?
        ): Boolean {
            if(null == request?.url) return false
            val showOverrideUrl = request.url.toString()
            try {
                if (!showOverrideUrl.startsWith("http://")
                    && !showOverrideUrl.startsWith("https://")) {
                    Intent(Intent.ACTION_VIEW, Uri.parse(showOverrideUrl)).apply {
                        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        view?.context?.applicationContext?.startActivity(this)
                    }
                    return true
                }
            }catch (e:Exception){
                return true
            }
            return super.shouldOverrideUrlLoading(view, request)
        }

        override fun onReceivedError(
            view: WebView?,
            request: WebResourceRequest?,
            error: WebResourceError?
        ) {
            super.onReceivedError(view, request, error)
            onReceivedError(error)
        }
    }
    var webView:WebView? = null
    val coroutineScope = rememberCoroutineScope()
    AndroidView(modifier = modifier,factory = { ctx ->
        WebView(ctx).apply {
            this.webViewClient = webViewClient
            this.webChromeClient = webViewChromeClient
            initSettings(this.settings)
            webView = this
            loadUrl(url)
        }
    })
    BackHandler {
        coroutineScope.launch {
            onBack(webView)
        }
    }
}

自定义TobRow的Indicator大小

由于在compose中TobRow的指示器宽度被写死,如果需要更改指示器宽度,则需要自己进行重写,将源码拷贝一份,然后根据自己需求进行定制,具体代码如下

@ExperimentalPagerApi
fun Modifier.customIndicatorOffset(
    pagerState: PagerState,
    tabPositions: List<TabPosition>,
    width: Dp
): Modifier = composed {
    if (pagerState.pageCount == 0) return@composed this

    val targetIndicatorOffset: Dp
    val indicatorWidth: Dp

    val currentTab = tabPositions[minOf(tabPositions.lastIndex, pagerState.currentPage)]
    val targetPage = pagerState.targetPage
    val targetTab = tabPositions.getOrNull(targetPage)

    if (targetTab != null) {
        val targetDistance = (targetPage - pagerState.currentPage).absoluteValue
        val fraction = (pagerState.currentPageOffset / max(targetDistance, 1)).absoluteValue

        targetIndicatorOffset = lerp(currentTab.left, targetTab.left, fraction)
        indicatorWidth = lerp(currentTab.width, targetTab.width, fraction).value.absoluteValue.dp
    } else {
        targetIndicatorOffset = currentTab.left
        indicatorWidth = currentTab.width
    }

    fillMaxWidth()
        .wrapContentSize(Alignment.BottomStart)
        .padding(horizontal = (indicatorWidth - width) / 2)
        .offset(x = targetIndicatorOffset)
        .width(width)
}

使用就变得很简单了,因为是采用modifier的扩展函数进行编写,而modifier在每一个compose组件都拥有,所以只需要在tabrow的指示器调用即可,具体代码如下

TabRow(
            ...
            indicator = { pos ->
                TabRowDefaults.Indicator(
                    color = BilibiliTheme.colors.tabSelect,
                    modifier = Modifier.customIndicatorOffset(
                        pagerState = pageState,
                        tabPositions = pos,
                        32.dp
                    )
                )
            }
            ...
      )

首页

整个首页页面由BottomNavbar构成,包含四个子界面,其中第一个界面又由两个子界面组成,通过TabRow+HorizontalPager完成子页面滑动,子页面分为推荐热门两个页面

推荐

推荐页面由上面的Banner和下方的LazyGridView组成,由于Compose中不允许同向滑动,所以就将Banner作为LazyGridView的一个item,进而进行包裹

Android Compose——一个简单的Bilibili APPAndroid Compose——一个简单的Bilibili APP

LazyGridView使用Paging3

由于在现在Compose版本中LazyGridView并不支持Paging3,所以如果有此类需求,则需要自己动手,具体代码如下

fun <T : Any> LazyGridScope.items(
    items: LazyPagingItems<T>,
    key: ((item: T) -> Any)? = null,
    span: ((item: T) -> GridItemSpan)? = null,
    contentType: ((item: T) -> Any)? = null,
    itemContent: @Composable LazyGridItemScope.(value: T?) -> Unit
) {
    items(
        count = items.itemCount,
        key = if (key == null) null else { index ->
            val item = items.peek(index)
            if (item == null) {
                //PagingPlaceholderKey(index)
            } else {
                key(item)
            }
        },
        span = if (span == null) null else { index ->
            val item = items.peek(index)
            if (item == null) {
                GridItemSpan(1)
            } else {
                span(item)
            }
        },
        contentType = if (contentType == null) {
            { null }
        } else { index ->
            val item = items.peek(index)
            if (item == null) {
                null
            } else {
                contentType(item)
            }
        }
    ) { index ->
        itemContent(items[index])
    }
}

热门

热门页面代码与推荐页面代码类似,此处不在阐述

Android Compose——一个简单的Bilibili APPAndroid Compose——一个简单的Bilibili APP

排行榜

排行界面与上述类似,Tab+HorizontalPager完成所有子页面滑动切换,此处也不在继续阐述

Android Compose——一个简单的Bilibili APPAndroid Compose——一个简单的Bilibili APP

搜索

搜索界面主要分为四个模块:搜索栏、热搜内容、搜索记录、搜索列表;搜索框内字符改变,搜索列表显示并以富文本显示,热搜内容展开与折叠、搜索记录内容展开与折叠、清空记录等操作都在ViewModel中完成,然后view通过监听VM中状态值进行重组

Android Compose——一个简单的Bilibili APPAndroid Compose——一个简单的Bilibili APP

模糊搜索

在搜索框内键入字符,然后通过字符的改变,获取相应的网络请求数据,最后通过AnimatedVisibility显示与隐藏搜索建议列表

Android Compose——一个简单的Bilibili APPAndroid Compose——一个简单的Bilibili APP

富文本

通过逐字匹配输入框内的字符与搜索建议item内容,然后输入框的字符存在搜索建议列表中的文字就加入高亮显示列表中,因为采用buildAnnotatedString,可以让文本显示多种不同风格,所以最后将字符内容区别为高亮颜色和普通文本两种文本,并让其进行显示

@Composable
fun RichText(
    selectColor: Color,
    unselectColor: Color,
    fontSize:TextUnit = TextUnit.Unspecified,
    searchValue: String,
    matchValue: String
){
    val richText = buildAnnotatedString {
        repeat(matchValue.length){
            val index = if (it < searchValue.length) matchValue.indexOf(searchValue[it]) else -1
            if (index == -1){
                withStyle(style = SpanStyle(
                    fontSize = fontSize,
                    color = unselectColor,
                )
                ){
                    append(matchValue[it])
                }
            }else{
                withStyle(style = SpanStyle(
                    fontSize = fontSize,
                    color = selectColor,
                )
                ){
                    append(matchValue[index])
                }
            }
        }
    }
    Text(
        text = richText,
        maxLines = 1,
        overflow = TextOverflow.Ellipsis,
        modifier = Modifier.fillMaxWidth(),
    )
}

搜索结果

搜索结果也是由ScrollableTabRow+HorizontalPager完成子页面的滑动切换,但是与上述不同的是,所展现的Tab与内容并不是固定,而是根据后端返回的数据进行自动生成的。由于其他子页面的内容都是由LazyColumn进行展现,而综合界面有需要将其他界面的数据进行集中,所以就必须LazyColumn嵌套LazyColumn,然后这在Compose中是不被允许的,所以就将子Page的LazyColumn,使用modifier.heightIn(max = screenHeight.dp)进行高度限制,高度可以取屏幕高度,并且多个item之间都是取屏幕高度,之间不会存在间隙

Android Compose——一个简单的Bilibili APPAndroid Compose——一个简单的Bilibili APP

视频详情

视频播放功能暂未实现完成,因为获取的API返回的URL进行播放一直为403,被告知权限不足,在网上进行多番查询未果,所以暂且搁置。视频库采用的Google的ExoPlayer

Android Compose——一个简单的Bilibili APPAndroid Compose——一个简单的Bilibili APP

合集

每个视频返回的内容数据格式一致,但具体内容不一致,有的视频存在排行信息、合集等,就通过AnimatedVisibility进行显示和隐藏,将所有结果进行列出,然后在ViewModel通过解析数据,并改变相应的状态值,view即可进行重组

Android Compose——一个简单的Bilibili APPAndroid Compose——一个简单的Bilibili APP

信息

Android Compose——一个简单的Bilibili APPAndroid Compose——一个简单的Bilibili APP

Coroutines进行网络请求管理,避免回调地狱

在日常开发中网络请求必不可少,在传统View+java开发中使用Retrifit或者okhttp进行网络请求最为常见,但大多数场景中,后一个API需要前一个API数据内字段值,此时就需要callback进行操作,回调一次获取代码依旧看起来简洁,可读,但次数一旦增多,则会掉入回调地狱。Google后续推出的协程完美解决此类问题,协程的主要核心就是“通过非阻塞的代码实现阻塞功能”,具体代码如下

添加suspend

以下为示例代码,通过给接口添加suspend标志符,告知外界次方法需要挂起

@GET("xxxxx")
    suspend fun getVideoDetail(@Query("aid")aid:Int):BaseResponse<VideoDetail>

withContext

getVideoDetail挂起函数返回一个字段值,然后通过withContext包裹,使其进行阻塞,然后将返回值进行返回,后续的getVideoUrl挂起函数就可以使用前一个接口返回的数据;需要注意的是,函数都需为suspend修饰的方法,并且在统一协程域中,否则会出现异常

 viewModelScope.launch(Dispatchers.Main) {
            try {
                withContext(Dispatchers.Main){
                    val cid = withContext(Dispatchers.IO){
                        getVideoDetail(_videoState.value.aid)
                    }
                    val url = withContext(Dispatchers.IO){
                        getVideoUrl(avid = _videoState.value.aid, cid = cid)
                    }
                    if (url.isNotEmpty()){
                        play(url)
                    }
                    getRelatedVideos(_videoState.value.aid)
                }
            }catch (e:Exception){
                Log.d("VDetailViewModel",e.message.toString())
            }
        }

Git项目链接

Git项目链接

此Demo并未完全完善,尤其是播放界面,由于采用Bilibili API获取的视频URL,在播放时一直返回403错误,被告知没有权限,在根据文档进行使用以及网上查询未果之后,只能暂且搁置此功能。文章来源地址https://www.toymoban.com/news/detail-402610.html

到了这里,关于Android Compose——一个简单的Bilibili APP的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 基于python requests库的bilibili爬虫简单尝试以及数据分析及可视化

    在初步了解了关于爬虫的课程之后,我也进行了一些自己的尝试。本文将从“爬取BiliBili Vtuber区直播信息为切入点,来探讨requests, re等库的基础应用。在爬取信息之后,本文将通过matplotlib以及pandas库做数据分析以及可视化 首先,我们先确认任务:打开Bilibili,在直播分区中选

    2024年02月05日
    浏览(28)
  • Android studio编写一个简单的登录界面

    1首先先创建一个空的activity项目,接着设置自己的项目名称,勾选上lacuncher 创建成功后点开 manifests 把刚刚创建的文件名下面的 intent-filter 这一行全部删除 然后点开res,复制一张图片,右键drawable点击粘贴,这里放的是图片资源,用于放置登录头像 然后点开layout文件,开始编

    2024年04月15日
    浏览(29)
  • Android studio学习感受加一个简单的登录注册

    作为一名使用Android Studio的学生,我也深有同感。在我看来,Android Studio是一款非常出色的开发工具先得感觉是Android Studio+Genymotion的组合比以前好用太多了。以前我记得eclipse要加各种jar包,文件夹也混乱的很。 然后是关于Activity和布局、控件,感觉跟网页前端很像,布局和控

    2024年02月02日
    浏览(32)
  • Android Studio制作一个简单的计算器APP

    虽然现在我们日常生活中很少用到计算器,但是第一次尝试在Android Studio上做一个计算器 程序设计步骤: (1)在布局文件中声明编辑文件框EditText,按钮Button等组件。 (2)在MainActivity中获取组件实例。 (3)通过swtich函数,判断输入的内容,并进行相应操作,通过getText()获

    2024年02月11日
    浏览(33)
  • 在 Android Studio 中创建一个简单的 QQ 登录界面

            打开 Android Studio,选择 \\\"Start a new Android Studio project\\\",然后填写应用程序名称、包名和保存路径等信息。接下来,选择 \\\"Phone and Tablet\\\" 作为您的设备类型,然后选择 \\\"Empty Activity\\\" 作为您的 Activity 模板。         在 Android Studio 中,布局文件用于指定应用程序的用

    2024年02月07日
    浏览(38)
  • Android Studio|使用SqLite实现一个简单的登录注册功能

    本学期学习了Android Studio这门课程,本次使用Android Studio自带的sqlite数据库实现一个简单的登录注册功能。 目录 一、了解什么是Android Studio? 二、了解什么是sqlite? 三、创建项目文件  四、创建活动文件和布局文件。 五、创建数据库,连接数据库  六、创建实体类,实现注

    2024年02月06日
    浏览(42)
  • 【移动开发学习】 Android Studio 编写一个简单的微信界面

    Android Studio简单还原微信ui 目标 实现3-4个tab的切换效果 技术需求 activity, xml, fragment, recyclerview 成果展示 其中联系人界面通过recyclerview实现了可以滑动列表           仓库地址 https://github.com/SmileEX/wecaht.git 实现过程 主要ui 第一步我们首先把微信的ui主体做出来,即这三个部分

    2024年02月08日
    浏览(40)
  • 关于B站(bilibili)对未登录用户视频观看进行暂停和弹窗的分析与简单解决方案

    于近日的某次更新后,B站(bilibili)网页端出现了一个新功能:当用户没有登录时,将对每个视频间隔性地(目前的情况是视频开始播放后的1分钟)进行 自动暂停并弹出登录窗口 。不得不说,这个功能使得使用体验极差,每个视频都要经历暂停和弹窗实在是让人不爽。有些

    2024年02月02日
    浏览(30)
  • 简介:在这篇教程中,我们将使用React.js框架创建一个简单的聊天机器人的前端界面,并利用Dialogflo

    作者:禅与计算机程序设计艺术 介绍及动机 聊天机器人(Chatbot)一直是互联网领域中的热门话题。而很多聊天机器人的功能都依赖于人工智能(AI)技术。越来越多的企业希望拥有自己的聊天机器人系统,从而提升自己的竞争力。为此,业界也出现了很多基于开源技术或云

    2024年02月06日
    浏览(42)
  • Android Studio:一个简单的计算器app的实现过程<初级>

    📌Android Studio 专栏正在持续更新中,案例的原理图解析、各种模块分析💖这里都有哦,同时也欢迎大家订阅专栏,获取更多详细信息哦✊✊✊ ✨个人主页:零小唬的博客主页 🥂欢迎大家 👍点赞 📨评论 🔔收藏 ✨作者简介:20级计算机专业学生一枚,来自宁夏,可能会去

    2024年02月01日
    浏览(92)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包