No ViewPager?

Some time ago, when Compose came out in the beta, I wanted to write and play a little bit about it. I wrote a few articles about composing the Compose app for Android.

  1. Explore the Compose version for Android
  2. Explore the Compose version for android play
  3. Compose implements pull-down refreshing and pull-up loading
  4. Compose Android Development Ultimate Challenge: Weather app

For Compose, check out the Github address: github.com/zhujiang521… And don’t forget the main branch.

We want to see you can look at the first few articles, although there is no direct correlation to this article, but after all there will be good.

Banner Banner Banner Banner Banner Banner Banner Banner Banner This wasn’t a big deal when I was writing Android, whether I was implementing it myself using ViewPager or using a tripartite library directly; Especially with tripartite libraries, adding a line of dependencies is almost trivial, but… Now what about using Compose?

Compose should have something similar to ViewPager in Compose, but I can’t find it. No? So what? Define yourself?

Where there is a silver lining!

When I was at a loss, I thought of taking a look at Google’s official Demo again, and maybe I could get some useful information!

Sure enough! In the official Jetcaster Demo, there is a custom control that looks like ViewPager: Pager.

Github.com/android/com…

Above is the address of the Jetcaster project, and below is Pager’s address so you don’t have to look for it:

Github.com/android/com…

Creating a common library

Since the official code has been posted above, the ViewPager should not be customized (mainly lazy) in line with the principle of not reinventing the wheel when it is already there. Directly take the official reference, and then their own simple implementation and encapsulation of the next, the following is the result of the style, we can make do:

Not bad, ha ha ha! Let’s see what we need to do to use this Banner:

The first step is definitely to add dependencies:

To start, add the following content to your build.gradle file at the root of your project:

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

Then you need to add dependencies to your build.gradle file in your Module:

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

Fill in the above version number in the Tag. The first available version is 1.3.3

Next, we need to define the Model of the Banner:

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

The Model is simple, it has one argument, it inherits from a BaseBannerBean, and some of you might say, well, what’s a BaseBannerBean? Don’t worry, we’ll talk later, for now!

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: hahaha:$item")}Copy the code

Is it so easy? Just pass in the defined items, yes, just pass in your data set, the parameters in the function object are the data that you call back, and just do what you want! Is not enchanted?? The indicator color in the above picture is too ugly, straight man aesthetic, don’t mind! Ha ha ha, rest assured, if you want to change the color of what can be directly changed! Take a closer look at what else is available!

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

Let’s take a look at each parameter and see what it means:

  • itemsThe data source, you can see that the data source uses generics, so why do we use generics here but let’s talk about it a little bit more, we just know that it has to be inherited from, rightBaseBannerBeanIt is ok
  • Repeat: Whether automatic rotation is allowed
  • Config: Some configuration parameters of the Banner
  • indicator: Banner indicator, you can use the defaultCircleIndicatorNumberIndicator, can also be customized, just need to inheritIndicatorCan.
  • onBannerClick:BannerThe click event will call back a generic parameter for everyone to use

With these parameters you can modify as many styles as you want!

Let the picture move

Here I chose to use Timer to make the image move! Take a look at the code:

val timer = Timer()
val mTimerTask: TimerTask = object : TimerTask() {
    override fun run(a) {
        viewModelScope.launch {
            if (pagerState.currentPage == pagerState.maxPage) {
                pagerState.currentPage = 0
            } else {
                pagerState.currentPage++
            }
            pagerState.fling(-1f) } } } timer? .schedule(mTimerTask,5000.3000)
Copy the code

If the current page number is the same as the maximum page number, manually set it to 0. Is it too simple? Hahaha, don’t think too much about it. It’s as simple as that. Then use the Fling method in pagerState to animate the picture! What the pagerState is is explained below.

BaseBannerBean

I wanted to say it to you piece by piece, but I thought it would be better to understand it step by step according to what WAS written.

If we look at items, we can see that it inherits from a BaseBannerBean. What is a BaseBannerBean? It’s not a thing, it’s just for abstraction, to be able to do unified image processing, without further ado, let’s look at the code:

/** * Base class of Banner Model */
abstract class BaseBannerBean {
    // Image resources can be urls, file paths, or drawable ids
    abstract val data: Any?
}
Copy the code

No, that’s it! That’s it! Only a data, what with notes also write, is the picture resources, in order to facilitate unified processing so just out!

In the beginning, I actually wrote three variables: a URL, a file path, and a drawable ID. Then I decided which one was not null, but later I decided to make it one. It’s just a picture.

Here’s how to use it step by step:

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)
    }
}
Copy the code

The Pager for Compose is the official ViewPager for Compose. It’s easy to use, but you need to pass in a pagerState that stores the maximum page number, minimum page number, and current page number. The creation method is simple:

val pagerState: PagerState = remember { PagerState() }
Copy the code

And I’m just going to insert pagerState directly like above.

BannerCard

Off topic, this little piece is talking about pictures… The BannerCard control is used to display images.

/** ** ** /** *@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: Loading Web images")
                    CoilImage(
                        data = img,
                        contentDescription = null,
                        modifier = imgModifier
                    )
                } else {
                    Log.d(TAG, "PostCardPopular: Loading local images")
                    val bitmap = BitmapFactory.decodeFile(img)
                    Image(
                        modifier = imgModifier,
                        painter = BitmapPainter(bitmap.asImageBitmap()),
                        contentDescription = "",
                        contentScale = ContentScale.Crop
                    )
                }
            }
            is Int -> {
                Log.d(TAG, "PostCardPopular: Loading local resource images")
                Image(
                    modifier = imgModifier,
                    painter = painterResource(bean.data as Int),
                    contentDescription = "",
                    contentScale = ContentScale.Crop
                )
            }
            else- > {throw IllegalArgumentException("Invalid parameter type: url, file path or drawable ID")}}}}Copy the code

Although the code looks ata lot, but the logic is not very clear, passed in the BaseBannerBean data if empty directly throw exception, because the image resources are empty what plane ah! Right? Now load the image directly according to different resources.

Two things in particular need to be said here:

  1. This parameter is required when the image resource is an image pathBitmapFactoryFirst toBitmapAnd then you need to putBitmapthroughBitmapExtension method ofasImageBitmapintoImageBitmap, so as to be inComposeImageThe use of.
  2. Network picture loading here used a tripartite loading librarycoil, what is the specific, but more introduction, we can go to Baidu, inKotlinIs still recommendedcoilAnd, of course,GlideAlso support theComposeYou can use it freely.

Local image

We’ve already tried web images in the beginning, but we’ve already mentioned that you can also use local images, so let’s try it out:

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()
}
Copy the code

Well, it feels easier than the pictures on the Internet, ha ha ha! Run it and have a look:

Gnome male – “! Do you notice anything different? Is the indicator on the left?! Hahaha, because the CircleIndicator has added BannerGravity.BottomLeft, so it is on the left. I will introduce this in detail below. There is no problem loading local images, and since they are local, they are much faster than loading network images!

Banner configuration class

A BannerPager argument. Let’s see what this class can change:

data class BannerConfig(
    / / banner level
    var bannerHeight: Dp = 210.dp,
    // The padding of the banner image
    var bannerImagePadding: Dp = 8.dp,
    // Shape for the banner image
    var shape: Shape = RoundedCornerShape(10.dp),
    // The interval between banner switching
    var intervalTime: Long = 3000
)
Copy the code

The height of the banner, the padding value around the distance, the shape of the image, and the interval between the banner and the image can be set here.

This we can also modify the inside of the value to see the effect!

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()
}
Copy the code

As you can see, we changed the default BannerConfig value to 250dp, the Padding value of the image to 0dp, and the rounded corner to 0. The switching time was changed to 1 second.

Is it basically in line with our hearts, and the loading speed has become a lot faster, rounded corners are gone, became a rectangle!

Banner indicator

This one needs to be explained well, in fact, the Banner does not have many things, just a picture area, the rest is the indicator, generally is the indicator of various styles of fancy!

I default to two types of indicators: the common circular indicator and the numeric indicator. I think you can see it up here.

Let’s see how the indicators are written and how you can customize them if you want!

/** * Pointer base class. If you want to customize the pointer, you need to inherit this class and implement the DrawIndicator method * don't forget to add it on the override method@ComposableNote * /
abstract class Indicator {

    abstract var gravity: Int

    @Composable
    abstract fun DrawIndicator(pagerState: PagerState)

}
Copy the code

Gravity: gravity: gravity: gravity: Gravity: Gravity: Gravity: Gravity: Gravity: Gravity: Gravity: Gravity: Gravity

/** * BannerGravity sets the pointer position */
object BannerGravity {

    // The bottom is centered
    const val BottomCenter = 0
    // Bottom left
    const val BottomLeft = 2
    // Bottom right
    const val BottomRight = 3

}
Copy the code

Why only define three? Because I think that’s all it takes… If there’s anything else, I’ll add it later.

Circular indicator

Let’s start with the circular indicator:

/** * circular indicator eg:. . * *@paramIndicatorColor Indicates the default color *@paramSelectIndicatorColor indicator Select the color *@paramIndicatorDistance Distance between indicators *@paramIndicatorSize Indicates the default circle size *@paramSelectIndicatorSize Indicator Select circle size *@paramGravity indicator position */
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)
                )
            }
        }
    }
}
Copy the code

Although the above code is a bit much, but the logic is actually very simple, loop to draw a circle, by PagrState to determine whether the current page, and then draw a different color.

As you can see, some configurations can also be changed in the circular indicator. If you don’t want to use the default, you can change it by parameter.

The code is very simple, directly through the Canvas to draw several circles, the above also said that you can define the color!

Let me tell you how I drew it… The outer part is wrapped with a for loop. Through PageState, you can obtain the number of pages and the current page number, then add a Canvas, obtain the width and height of the Canvas, obtain the current color, and obtain the coordinate points to be placed according to the set gravity. Finally, draw directly on it!

Circular indicators are all shown in the picture above, and they are also shown on the left. Here’s what they look like on the right:

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

How about it? It’s not ugly! Ha, ha, ha.

Digital indicator

Digital indicators, like circular indicators, inherit from self-indicators:

/** * number indicator, display number indicator eg: 1/5 **@paramBackgroundColor digital indicator backgroundColor *@paramNumberColor specifies the numberColor *@paramCircleSize Indicates the radius of the background circle *@paramFontSize Specifies the page text size *@paramGravity indicator position */
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
                )
            }
        }
    }

}
Copy the code

I also wanted to use Canvas to draw here, but after thinking about it, some friends might not like drawing, so I used Box for layout, depending on your needs, any writing method is ok, you can also draw on Canvas here. Similarly, some configuration changes can be made to the numeric indicator. If you do not want to use the default, you can change it by parameter.

As can be seen from the above code, the number indicator can also be set to the left, middle and right of three places.

The small end

At this point a Banner is done, the functionality is almost complete, but…

Fear nothing but! But it is not particularly nice, but it doesn’t matter, has given you the interface to open, directly inherit their own drawing on the line, and the Banner properties can be set, in fact, some of the above has not been shown, you can modify each parameter try, there will be different small surprise!

In fact, there are a lot of things not written, such as the animation of the picture switch… A good library can not be written in a day or two, I hope you can give me more advice.

Finally, the Github library address: github.com/zhujiang521…

If you are helpful, don’t forget to like ah, Github library also needs your Star! Thanks so much!!

That’s it. See you next time!