🔥🔥🔥Android Compose Banner!!! 安卓 Compose 版本的 Banner

Overview

Banner

打造 Compose 版本的 Banner

没有 ViewPager ?

之前写安卓的时候这都不是事,不管是自己使用 ViewPager 实现又或者是直接使用三方库,都不是什么难事;特别是使用三方库,添加一行依赖基本就没啥事了,但。。。现在使用 Compose 该怎么搞?

本来想着 Compose 中怎么着也应该有类似于 ViewPager 的东西,但找遍了愣是没找见,什么情况?没有?那怎么搞?自己定义?不是个事啊!

柳暗花明!

正当一筹莫展的时候,又想起来再看看 Google 官方给的 Demo 吧,没准能获得一些有用的信息呢!

果然!在官方 Jetcaster 这个 Demo 中竟然自定义有类似 ViewPager 的控件:Pager ,别的不说,先来贴下地址:

https://github.com/android/compose-samples/tree/1630f6b35ac9e25fb3cd3a64208d7c9afaaaedc5/Jetcaster

上面是 Jetcaster 项目的地址,下面再贴下 Pager 的地址吧,免得大家还得找:

https://github.com/android/compose-samples/blob/1630f6b35ac9e25fb3cd3a64208d7c9afaaaedc5/Jetcaster/app/src/main/java/com/example/jetcaster/util/Pager.kt#L130

创建公共库

上面既然已经贴了官方的代码了,本着有现成就不重复制造轮子的原则,就不自定义 ViewPager 了(主要是懒)。直接把官方的拿过来借鉴下,然后再自己简单实现和封装了下,下面是成果样式,大家凑合看:

还不错吧,哈哈哈!来看看使用这个 Banner 需要怎么做吧:

第一步肯定需要添加依赖了:

首先需要在你项目根目录下的 build.gradle 文件中添加以下内容:

	allprojects {
		repositories {
			...
			maven { url 'https://jitpack.io' }
		}
	}

然后需要在你的 module 中的 build.gradle 文件中添加依赖:

	dependencies {
	        implementation 'com.github.zhujiang521:Banner:Tag'
	}

上面的 Tag 处需要填写上面的版本号,第一个可用的版本是 1.3.3

接下来需要先定义下 BannerModel

data class BannerBean(
    override val data: Any? = null
) : BaseBannerBean()

Model 很简单,只有一个参数,继承自 BaseBannerBean ,可能有人就说了,BaseBannerBean 是个啥玩意?别着急,后面会说的,现在先这样用!

Toast.makeText(context, "item:$item", Toast.LENGTH_SHORT).show() Log.e(TAG, "Notes: 哈哈哈:$item") } ">
val items = arrayListOf(
            BannerBean(
                "https://wanandroid.com/blogimgs/8a0131ac-05b7-4b6c-a8d0-f438678834ba.png",
            ),
            BannerBean(
                "https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png",
            ),
            BannerBean(
                "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",
            ),
            BannerBean(
                "https://www.wanandroid.com/blogimgs/90c6cc12-742e-4c9f-b318-b912f163b8d0.png",
            ),
        )

BannerPager(
    items = items
) { item ->
    Toast.makeText(context, "item:$item", Toast.LENGTH_SHORT).show()
    Log.e(TAG, "Notes: 哈哈哈:$item")
}

怎么样,是不是 so easy ?只需要传入定义好的 items ,没错,只需要传入你的数据集合就可以了,函数对象中的参数是回调回来的数据,直接做你想做的事就可以了!是不是心动了???上面图中的 indicator 颜色也太丑了,直男审美,大家别介意!哈哈哈,放心吧,如果想换颜色什么的都可以直接换!快来详细看看还有什么功能吧!

@Composable
fun <T : BaseBannerBean> BannerPager(
    items: List<T> = arrayListOf(),
    repeat: Boolean = true,
    config: BannerConfig = BannerConfig(),
    indicator: Indicator = CircleIndicator(),
    onBannerClick: (T) -> Unit
)

来一个一个参数看看啥意思吧:

  • items:数据源,可以看到数据源使用到了泛型,这里为什么使用泛型下面再说,这里只要知道必须是继承自 BaseBannerBean 就可以了
  • repeat:是否允许自动轮播
  • config:Banner 的一些配置参数
  • indicator:Banner 指示器,你可以使用默认的 CircleIndicatorNumberIndicator ,也可以自定义,仅需要继承 Indicator 即可。
  • onBannerClickBanner 的点击事件,会回调回一个泛型参数供大家使用

通过上面的这些参数可以修改很多你想要的样式!

图片动起来

这里我选择使用 Timer 来实现让图片动起来!来看看代码吧:

val timer = Timer()
val mTimerTask: TimerTask = object : TimerTask() {
    override fun run() {
        viewModelScope.launch {
            if (pagerState.currentPage == pagerState.maxPage) {
                pagerState.currentPage = 0
            } else {
                pagerState.currentPage++
            }
            pagerState.fling(-1f)
        }
    }
}

timer?.schedule(mTimerTask, 5000, 3000)

就是当当前页码和最大页码相同的时候手动置为 0 就可以了,嗯,就这!是不是太简单了,哈哈哈,别想太多,其实就这么简单。然后通过 pagerState 中的 fling 方法让图片动起来! pagerState 是什么在下面会有介绍的。

BaseBannerBean

本来想一块一块给大家说的,但想了想还是直接根据写好的一步步深入比较好理解。

咱们先来看看 items ,可以看到它继承自 BaseBannerBean ,那这个 BaseBannerBean 又是个什么东西呢?它不是个东西,它只是为了抽象,为了能统一对图片做处理,废话不多说,直接看代码:

/**
 * Banner Model 的基类
 */
abstract class BaseBannerBean {
    // 图片资源 可以是:url、文件路径或者是 drawable id
    abstract val data: Any?
}

对,没了,就这么点内容!只有一个 data ,有什么用注释也写了,就是图片资源,为了方便统一做处理所以才抽出来的!

最开始的时候这块我其实写了三个变量:一个 url ,一个文件路径,一个 drawable id ,然后再判断哪个不为空进行处理,但是后来想了想弄成一个就可以了啊,一个图片而已嘛!

下面来一步一步看看怎么使用吧:

Pager(
    state = pagerState,
    modifier = Modifier.fillMaxWidth().height(config.bannerHeight)
) {
    val item = items[page]
    BannerCard(
        bean = item,
        modifier = Modifier.fillMaxSize().padding(config.bannerImagePadding),
        shape = config.shape
    ) {
        Log.d(TAG, "item is :${item.javaClass}")
        onBannerClick(item)
    }
}

上面代码中的 Pager 就是上面提到的官方的 Compose 版本的 ViewPager ,使用很简单,但是需要传入一个 pagerState ,这个 pagerState 里面保存着最大页码、最小页码和当前页码,随时更新状态,创建方法很简单:

val pagerState: PagerState = remember { PagerState() }

然后把 pagerState 直接像上面那样穿进去就行。

BannerCard

跑题了跑题了,这一小块说的是图片。。。。继续看,上面代码中的 BannerCard 就是显示图片的控件,来看看怎么写的吧:

{ val img = bean.data as String if (img.contains("https://") || img.contains("http://")) { Log.d(TAG, "PostCardPopular: 加载网络图片") CoilImage( data = img, contentDescription = null, modifier = imgModifier ) } else { Log.d(TAG, "PostCardPopular: 加载本地图片") val bitmap = BitmapFactory.decodeFile(img) Image( modifier = imgModifier, painter = BitmapPainter(bitmap.asImageBitmap()), contentDescription = "", contentScale = ContentScale.Crop ) } } is Int -> { Log.d(TAG, "PostCardPopular: 加载本地资源图片") Image( modifier = imgModifier, painter = painterResource(bean.data as Int), contentDescription = "", contentScale = ContentScale.Crop ) } else -> { throw IllegalArgumentException("参数类型不符合要求,只能是:url、文件路径或者是 drawable id") } } } } ">
/**
 * Banner 图片展示卡片
 *
 * @param bean banner Model
 * @param modifier
 */
@Composable
fun <T : BaseBannerBean> BannerCard(
    bean: T,
    modifier: Modifier = Modifier,
    shape: Shape = RoundedCornerShape(10.dp),
    onBannerClick: () -> Unit,
) {
    if (bean.data == null) {
        throw NullPointerException("Url or imgRes or filePath must have a not for empty.")
    }

    Card(
        shape = shape,
        modifier = modifier
    ) {
        val imgModifier = Modifier.clickable(onClick = onBannerClick)
        when (bean.data) {
            is String -> {
                val img = bean.data as String
                if (img.contains("https://") || img.contains("http://")) {
                    Log.d(TAG, "PostCardPopular: 加载网络图片")
                    CoilImage(
                        data = img,
                        contentDescription = null,
                        modifier = imgModifier
                    )
                } else {
                    Log.d(TAG, "PostCardPopular: 加载本地图片")
                    val bitmap = BitmapFactory.decodeFile(img)
                    Image(
                        modifier = imgModifier,
                        painter = BitmapPainter(bitmap.asImageBitmap()),
                        contentDescription = "",
                        contentScale = ContentScale.Crop
                    )
                }
            }
            is Int -> {
                Log.d(TAG, "PostCardPopular: 加载本地资源图片")
                Image(
                    modifier = imgModifier,
                    painter = painterResource(bean.data as Int),
                    contentDescription = "",
                    contentScale = ContentScale.Crop
                )
            }
            else -> {
                throw IllegalArgumentException("参数类型不符合要求,只能是:url、文件路径或者是 drawable id")
            }
        }
    }
}

虽然代码看着不少,但是逻辑是不是很清晰,传进来的 BaseBannerBean 中的 data 如果为空的话直接抛异常,因为图片资源都为空了还搞什么飞机啊!对不?下面就根据不同资源直接加载图片就行了。

这里需要特别说的有两点:

  1. 当图片资源为图片路径的时候,需要通过 BitmapFactory 先转成 Bitmap ,然后还需要把 Bitmap 通过 Bitmap 的扩展方法 asImageBitmap 转成 ImageBitmap ,这样才能在 ComposeImage 中使用。
  2. 网络图片的加载这里用到了一个三方加载库 coil ,具体是什么就不过多介绍了,大家可以去百度下,在 Kotlin 中还是推荐使用 coil ,当然 Glide 也支持了 Compose ,大家可以随便使用。

本地图片

在最开始的时候已经试了网络图片,这里咱们刚说过还可以使用本地图片,来试试吧:

val items2 = arrayListOf(
    BannerBean(R.drawable.banner1),
    BannerBean(R.drawable.banner2),
    BannerBean(R.drawable.banner3),
    BannerBean(R.drawable.banner4),
)

BannerPager(
    modifier = Modifier.padding(top = 10.dp),
    items = items2,
    indicator = CircleIndicator(gravity = BannerGravity.BottomLeft)
) { item ->
    Toast.makeText(context, "item:$item", Toast.LENGTH_SHORT).show()
}

嗯,感觉比网络图片还简单啊,哈哈哈!运行下看看吧:

嘿嘿嘿!大家有没有发现什么不一样?指示器是不是跑到左边了!哈哈哈,因为上面 CircleIndicator 中加了 BannerGravity.BottomLeft ,所以就在左边,这个下面会详细给大家介绍。而且加载本地图片完全没有问题,而且由于是本地的,比加载网络图片快得多!

Banner配置类

上面 BannerPager 中参数的一个类型,咱们来看看这个类可以修改什么东西吧:

data class BannerConfig(
    // banner 高度
    var bannerHeight: Dp = 210.dp,
    // banner 图片距离四周的 padding 值
    var bannerImagePadding: Dp = 8.dp,
    // banner 图片的 shape
    var shape: Shape = RoundedCornerShape(10.dp),
    // banner 切换间隔时间
    var intervalTime: Long = 3000
)

注释中基本都写了,banner 高度 、距离四周的 padding 值、图片的 shape 以及切换间隔时间都可以在这里进行设置,当然不设置也可以,不设置的话就是默认值,也够用了!

这块咱们也可以修改下里面的值看看效果!

BannerPager(
    items = items2,
    indicator = CircleIndicator(gravity = BannerGravity.BottomLeft),
    config = BannerConfig(
        bannerHeight = 250.dp,
        bannerImagePadding = 0.dp,
        shape = RoundedCornerShape(0),
        intervalTime = 1000
    )
) { item ->
    Toast.makeText(context, "item:$item", Toast.LENGTH_SHORT).show()
}

可以看到上面咱们修改了 BannerConfig 的默认值,将高度改为了 250dp,图片的 Padding 值改为了 0dp,圆角也设置为了 0,切换时间改为了 1 秒,来看看效果吧:

是不是基本符合咱们心中的样子,而且加载速度变快了许多,圆角也没了,成了长方形!

Banner 指示器

这一块需要好好说说,其实 Banner 并没有多少东西,也就一个图片区域,剩下的就是指示器了,一般都是指示器各种样式花里胡哨的!

我在这里一共默认了两种指示器,一种是常见的圆形的指示器,另一种是数字的指示器。出来的效果大家应该在上面都看见了。

来看看指示器是怎么写的,如果你想自定义的话该怎么做!

/**
 * 指示器基类,如果需要自定义指示器,需要继承此类,并实现 [DrawIndicator] 方法
 * 别忘了在重写方法上添加 @Composable 注解
 */
abstract class Indicator {

    abstract var gravity: Int

    @Composable
    abstract fun DrawIndicator(pagerState: PagerState)

}

上面代码看注释就能明白,这是一个抽象类,是所有 Banner 指示器的基类,里面有一个 gravity ,我定义了三个:

/**
 * BannerGravity 设置指示器位置
 */
object BannerGravity {

    // 底部居中
    const val BottomCenter = 0
    // 底部左边
    const val BottomLeft = 2
    // 底部右边
    const val BottomRight = 3

}

为什么只定义三个呢?因为我觉得也就这几种情况了。。。如果还有别的的话,之后我再加。

圆形指示器

接下来先来看看圆形指示器的写法吧:

/**
 * 圆形指示器 eg:。。. 。。
 *
 * @param indicatorColor 指示器默认颜色
 * @param selectIndicatorColor 指示器选中颜色
 * @param indicatorDistance 指示器之间的距离
 * @param indicatorSize 指示器默认圆大小
 * @param selectIndicatorSize 指示器选中圆大小
 * @param gravity 指示器位置
 */
class CircleIndicator(
    var indicatorColor: Color = Color(30, 30, 33, 90),
    var selectIndicatorColor: Color = Color.Green,
    var indicatorDistance: Int = 50,
    var indicatorSize: Float = 10f,
    var selectIndicatorSize: Float = 13f,
    override var gravity: Int = BottomCenter,
) : Indicator() {

    @Composable
    override fun DrawIndicator(pagerState: PagerState) {
        for (pageIndex in 0..pagerState.maxPage) {
            Canvas(modifier = Modifier.fillMaxSize()) {
                val canvasWidth = size.width
                val canvasHeight = size.height
                val color: Color
                val inSize: Float
                if (pageIndex == pagerState.currentPage) {
                    color = selectIndicatorColor
                    inSize = selectIndicatorSize
                } else {
                    color = indicatorColor
                    inSize = indicatorSize
                }
                val start = when (gravity) {
                    BottomCenter -> {
                        val width = canvasWidth - pagerState.maxPage * indicatorDistance
                        width / 2
                    }
                    BottomLeft -> {
                        100f
                    }
                    BottomRight -> {
                        canvasWidth - pagerState.maxPage * indicatorDistance - 100f
                    }
                    else -> 100f
                }
                drawCircle(
                    color,
                    inSize,
                    center = Offset(start + pageIndex * indicatorDistance, canvasHeight)
                )
            }
        }
    }
}

上面代码虽然有点多,但是逻辑其实很简单,循环画圆,通过 PagrState 来判断是否是当前页面,然后画不同的颜色。

可以看到,圆形指示器中也可以修改一些配置,如果不想用默认的完全可以通过参数进行修改。

代码很简单,直接通过 Canvas 画了几个圆,上面也说了,颜色大家可以自己进行定义!

还是说一下怎么画的吧。。。最外面包裹着一个 for 循环,通过 PageState 可以获取到页面的数量及当前页码,然后添加一个 Canvas ,获取下 Canvas 的宽和高,然后获取当前的颜色,再根据设置的 gravity 获取下放置的坐标点,最后直接画上去就可以了!

圆形指示器上面图中的都是,也展示了在左边的样式,这里展示下在右边的样子吧:

BannerPager(
    modifier = Modifier.padding(top = 10.dp),
    items = items2,
    indicator = CircleIndicator(gravity = BannerGravity.BottomRight)
) { item ->
    Toast.makeText(context, "item:$item", Toast.LENGTH_SHORT).show()
}

怎么样,不丑吧!哈哈哈。

数字指示器

数字指示器和圆形指示器一样,都继承自 Indicator

/**
 * 数字指示器,显示数字的指示器  eg: 1/5
 *
 * @param backgroundColor 数字指示器背景颜色
 * @param numberColor 数字的颜色
 * @param circleSize 背景圆的半径
 * @param fontSize 页码文字大小
 * @param gravity 指示器位置
 */
class NumberIndicator(
    var backgroundColor: Color = Color(30, 30, 33, 90),
    var numberColor: Color = Color.White,
    var circleSize: Dp = 35.dp,
    var fontSize: TextUnit = 15.sp,
    override var gravity: Int = BottomRight,
) : Indicator() {

    @Composable
    override fun DrawIndicator(pagerState: PagerState) {
        val alignment: Alignment = when (gravity) {
            BannerGravity.BottomCenter -> {
                Alignment.BottomCenter
            }
            BannerGravity.BottomLeft -> {
                Alignment.BottomStart
            }
            BottomRight -> {
                Alignment.BottomEnd
            }
            else -> Alignment.BottomEnd
        }
        Box(modifier = Modifier.fillMaxSize().padding(10.dp), contentAlignment = alignment) {
            Box(
                modifier = Modifier.size(circleSize).clip(CircleShape)
                    .background(color = backgroundColor),
                contentAlignment = Alignment.Center
            ) {
                Text(
                    "${pagerState.currentPage + 1}/${pagerState.maxPage + 1}",
                    color = numberColor,
                    fontSize = fontSize
                )
            }
        }
    }

}

这里原本也想使用 Canvas 画一下的,但想了想有的朋友可能不喜欢画,所以就使用 Box 进行布局了,看大家需要了,哪种写法都是可以的,大家也可以在这里也通过 Canvas 画一下。同样的,数字指示器中也可以修改一些配置,如果不想用默认的完全可以通过参数进行修改。

从上面代码不难看出,数字指示器也可以设置左中右三个地方,就不一一设置了,给大家看一下最常用的右边的样式吧:

小结尾

到这里为止一个 Banner 已经完成了,功能都差不多完成了,但是。。。

凡事就怕但是!但并不是特别好看,不过也无所谓了,已经将接口给大家放开了,直接继承自己画就行了,而且 Banner 中的属性都可以进行设置,其实上面有些还没有展示,大家可以每个参数都修改试试,会有不一样的小惊喜的!

其实还有很多没有写到的内容,比如图片切换的动画。。。一个优秀的库不是一两天就能写完的,希望大家能多给我提点建议。

对大家如果有帮助的话别忘了点 Star 啊!感激不尽!!!

You might also like...
An android library for displaying fps from the choreographer and percentage of time with two or more frames dropped
An android library for displaying fps from the choreographer and percentage of time with two or more frames dropped

DEPRECATED TinyDancer is deprecated. No more development will be taking place. Check out the Google Android developer documentation for UI performance

Memory safer implementation of android.os.Handler
Memory safer implementation of android.os.Handler

Android Weak Handler Memory safer implementation of android.os.Handler Problem Original implementation of Handler always keeps hard reference to handl

Android Library to help you with your runtime Permissions.
Android Library to help you with your runtime Permissions.

PermissionHelper Android Library to help you with your runtime Permissions. Demo Android M Watch it in action. Pre M Watch it in action. Nexus 6 (M) N

Android validation library which helps developer boil down the tedious work to three easy steps.
Android validation library which helps developer boil down the tedious work to three easy steps.

AwesomeValidation Introduction Implement validation for Android within only 3 steps. Developers should focus on their awesome code, and let the librar

📄The reliable, generic, fast and flexible logging framework for Android
📄The reliable, generic, fast and flexible logging framework for Android

logback-android v2.0.0 Overview logback-android brings the power of logback to Android. This library provides a highly configurable logging framework

Android framework for node.js applications

Introduction Anode is an embryonic framework for running node.js applications on Android. There are two main parts to this: a port of node.js to the A

It makes a preview from an url, grabbing all the information such as title, relevant texts and images. This a version for Android of my web link preview https://github.com/LeonardoCardoso/Link-Preview
It makes a preview from an url, grabbing all the information such as title, relevant texts and images. This a version for Android of my web link preview https://github.com/LeonardoCardoso/Link-Preview

LeoCardz Link Preview for Android It makes a preview from an url, grabbing all the information such as title, relevant texts and images. Visual Exampl

AndroidPermissions 4.2 0.0 Java Android M was added to check Permission. but Permission check processing is so dirty.

Android Permissions Checker Android M was added to check Permission. but Permission check processing is so dirty. This Project is to be simple, Checki

A plug and play ;) android library for displaying a
A plug and play ;) android library for displaying a "rate this app" dialog

Easy Rating Dialog This lib provides a simple way to display an alert dialog for rating app. Default conditions to show: User opened the app more than

Releases(2.4.0)
Owner
朱江
Android Tony
朱江
Makes Google play in app purchase library (BillingClient) into a flowable that can easily be used in compose world

Billy the android Our goal is to make a modern api of BillingClient so that it is easier to use in compose world. This library is still early beta and

Stefan Wärting 16 Dec 14, 2022
Highly experimental predefined Bootstrap functions to use in Compose Web

bootstrap-compose Highly experimental predefined Bootstrap functions to use in Compose Web Install This package is uploaded to MavenCentral. repositor

Philip Wedemann 45 Jan 6, 2023
Starter-Android-Library - Starter Android Library is an Android Project with Modular Architecture.

Starter-Android-Library - Starter Android Library is an Android Project with Modular Architecture.

OpenBytes 1 Feb 18, 2022
SL4A brings scripting languages to Android by allowing you to edit and execute scripts and interactive interpreters directly on the Android device.

#Scripting Layer for Android (SL4A) SL4A brings scripting languages to Android by allowing you to edit and execute scripts and interactive interpreter

Damon Kohler 2.3k Dec 23, 2022
****. Use the native and support library variants instead - https://developer.android.com/guide/topics/ui/look-and-feel/fonts-in-xml.html. An android library that makes it easy to add custom fonts to edittexts and textviews

Add to your project Add this line to your dependencies in build.gradle compile 'in.workarounds.typography:typography:0.0.8' Using the views There are

Workarounds 43 Nov 6, 2021
ZXing ("Zebra Crossing") barcode scanning library for Java, Android

Project in Maintenance Mode Only The project is in maintenance mode, meaning, changes are driven by contributed patches. Only bug fixes and minor enha

ZXing Project 30.5k Dec 27, 2022
RxJava binding APIs for Android's UI widgets.

RxBinding RxJava binding APIs for Android UI widgets from the platform and support libraries. Download Platform bindings: implementation 'com.jakewhar

Jake Wharton 9.7k Jan 6, 2023
A gradle plugin for getting java lambda support in java 6, 7 and android

Gradle Retrolambda Plugin This plugin will automatically build your java or android project with retrolambda, giving you lambda goodness on java 6 or

Evan Tatarka 5.3k Jan 5, 2023
A comprehensive tutorial for Android Data Binding

精通 Android Data Binding 更多干货可移步至个人主页 QQ 交流群:324112728 ,或者点击链接加入QQ群 官方虽然已经给出了教程 - Data Binding Guide (中文版 - Data Binding(数据绑定)用户指南) ,但是实践之后发现槽点实在太多,于是就

Fei Liang 2.6k Dec 6, 2022
A Job Queue specifically written for Android to easily schedule jobs (tasks) that run in the background, improving UX and application stability.

Development in this repository is stopped. Future development continues on https://github.com/yigit/android-priority-jobqueue ========================

Path Mobile Inc Pte. Ltd. 2.4k Dec 9, 2022