打造 Compose 版本的 Banner_compose banner-程序员宅基地

技术标签: compose  compose banner  Kotlin、JetPack  android  banner  viewpager  

没有 ViewPager ?

前段时间 Compose 出来 beta 版本的时候就想着写写玩一玩,把自己写的玩安卓重构成 Compose 版本的,于是就写了几篇文章:

  1. 初探 Compose 版本的玩安卓
  2. 再探 Compose 版本的玩安卓
  3. Compose 实现下拉刷新和上拉加载
  4. Compose Android 开发终极挑战赛: 天气应用

如果想学习 Compose 的话,可以去看看,里面有详细的代码供你参考:Github 地址:github.com/zhujiang521…,别忘了是 main 分支。

大家想看可以先看看前几篇文章,虽然对这篇文章没有直接的关联,但毕竟会有好处的。

这篇说什么大家看题目应该就知道了,就是 Banner 。之前写安卓的时候这都不是事,不管是自己使用 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 了(主要是懒)。直接把官方的拿过来借鉴下,然后再自己简单实现和封装了下,下面是成果样式,大家凑合看:

banner1

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

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

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

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

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

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

接下来需要先定义下 BannerModel

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

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

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 就是显示图片的控件,来看看怎么写的吧:

/**
 * 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()
}

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

banner2

嘿嘿嘿!大家有没有发现什么不一样?指示器是不是跑到左边了!哈哈哈,因为上面 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 秒,来看看效果吧:

banner3

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

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()
}

banner4

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

数字指示器

数字指示器和圆形指示器一样,都继承自 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 画一下。同样的,数字指示器中也可以修改一些配置,如果不想用默认的完全可以通过参数进行修改。

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

banner5

小结尾

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

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

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

最后放一下 Github 库的地址吧:https://github.com/zhujiang521/Banner

对大家如果有帮助的话别忘了点赞啊,Github 库也需要大家的 Star !感激不尽!!!

就这样,下回见!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/haojiagou/article/details/115325188

智能推荐

Arthas 开源 Java 诊断工具(服务器反编译class)_arthas 反编译命令-程序员宅基地

文章浏览阅读2.1k次。arthas 使用服务器环境编译类#1、进入arthas目录cd /root/arthas-packaging-3.3.9-bin#2、执行arthas命令java -jar arthas-boot.jar#3、找到自己需要进入的java进程,输入对应前面的序号【比如 1】1#4、查询自己所找的对应的类加载器hash码【后继用于编译】,比如我要找的类为com.tansun.tcp.sys.msg.action.BaseMsgSysAction输入下方命令:sc -d com._arthas 反编译命令

tc275单片机的内核_英飞凌TC275_bootloader源码 TC275单片机基于autosar的bootloader工程代码(Bootloader engineering code of tc...-程序员宅基地

文章浏览阅读1.4k次。压缩包 : f9a27655e546fd0d4bf5ed97eda6479f.zip 列表英飞凌TC275_bootloader源码/英飞凌TC275_bootloader源码/.cproject英飞凌TC275_bootloader源码/.htcproject英飞凌TC275_bootloader源码/.project英飞凌TC275_bootloader源码/.settings/英飞凌TC27..._英飞凌tc275芯片bootloader开发

SimpleFOC(三)—— AS5600角度读取_as5600磁编码器中文手册-程序员宅基地

文章浏览阅读3.2w次,点赞25次,收藏137次。目录一、硬件介绍1、磁编码器说明:2、硬件连接二、程序演示1、模拟电压获取角度2、I2C通信获取角度三、程序拓展一、硬件介绍1、磁编码器说明:  ◆AS5600与两极磁铁配对,可以输出12位分辨率的磁性旋转位置,支持IIC通信,还可以输出模拟电压和PWM信号。官方例程中主要演示了模拟电压和IIC通信两种角度获取方式。  ◆模拟电压模式,Aout引脚输出0—5V对应0°—360°,  ◆I2C模式,读取0x0C/0x0D两个寄存器,获取12bits的角度值,0—4096对应0°—360°,2_as5600磁编码器中文手册

Windows恢复Grub引导,用grub安装ubuntu-程序员宅基地

文章浏览阅读86次。http://www.linuxidc.com/wap.aspx?nid=18027&p=&cp=&cid=http://m.blog.chinaunix.net/uid-22197900-id-359250.htmlhttp://zhidao.baidu.com/question/147900468.html?fr=ala&word=Grub%20%E5%AE..._在windowa 修复grub 引导

lua游戏代码_在游戏中如何使用LUA脚本语言_lua如何用一段代码在游戏屏幕上添加一个按钮-程序员宅基地

文章浏览阅读3.6k次。当你希望在你的游戏开始的时候读取一些信息,以配置你的游戏,这些信息通常都是放到一个文本文件中,在你的游戏启动的时候,你需要打开这个文件,然后解析字符串,找到所需要的信息。或许你认为这样就足够了,为什么还要使用Lua呢?应用于“配置”这个目的,Lua提供给你更为强大,也更为灵活的表达方式,在上一种方式中,你无法根据某些条件来配置你的游戏,Lua提供给你灵活的表达方式,你可以类似于这样来配置你的游戏:ifplayer:is_dead()then_lua如何用一段代码在游戏屏幕上添加一个按钮

LINUX下MATLAB MEX编译的问题_matlab mex编译linux-程序员宅基地

文章浏览阅读3.8k次。最近跑一个程序,是matlab和c混合编程的,而且调用了一些Linux下特有的库文件,所以只能在linux下运行。但是ubuntu里安装的Matlab r2013a 出现了gcc版本问题。matlab r2013a只支持gcc4.4, 而ubuntu的gcc已经更新到4.8.2所以为了方便,只好安装4.4版的编译器, 包括gcc, g++, gfortran安装命令_matlab mex编译linux

随便推点

Centos7搭建maven私服nexus3-程序员宅基地

文章浏览阅读718次,点赞28次,收藏28次。针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。上述的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题(都整理成文档,小部分截图)以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取![外链图片转存中…(img-Baks9FZr-1713070530762)]上述的面试题答案都整理成文档笔记。

Python入门习题(43)——CCF CSP认证考试真题:小明放学_csp小明放学python-程序员宅基地

文章浏览阅读1.2k次。CCF CSP认证考试真题(201812-2):小明放学问题描述解题思路参考答案测试用例小结问题描述试题编号: 201812-2试题名称: 小明放学时间限制: 1.0s内存限制: 512.0MB问题描述:题目背景  汉东省政法大学附属中学所在的光明区最近实施了名为“智慧光明”的智慧城市项目。具体到交通领域,通过“智慧光明”终端,可以看到光明区所有红绿灯此时此刻的状态。小明的学校也安..._csp小明放学python

ajax实现留言板功能 -_请根据ssm框架,用ajax写一个互动留言功能的完整代码-程序员宅基地

文章浏览阅读4.6k次,点赞2次,收藏12次。首先理解ajax是实现页面无刷新效果,留言就是要这种效果,当发布留言时候直接展示留言内容在下面.好了直接上代码1.新建一个控制器,内容如下: &lt;?phpnamespace app\index\controller;use think\Controller;class Index extends Controller{ public function in..._请根据ssm框架,用ajax写一个互动留言功能的完整代码

MobileNet实战:基于 MobileNet 的人脸微表情分类(Caffe)_嘟嘴表情识别开源基于人脸关键点-程序员宅基地

文章浏览阅读2.3k次,点赞3次,收藏15次。MobileNet实战:基于 MobileNet 的人脸微表情分类(Caffe)这一部分内容总共由下面四篇文章组成:MobileNet 进化史: 从 V1 到 V3(V1篇)MobileNet 进化史: 从 V1 到 V3(V2篇)MobileNet 进化史: 从 V1 到 V3(V3篇)MobileNet实战:基于 MobileNet 的人脸表情分类1. 前言前面我们已经简要介..._嘟嘴表情识别开源基于人脸关键点

ruoyi-ui下载依赖报错npm ERR! cb() never called!_error running 'npm install in ruoyi-vue-master\ruo-程序员宅基地

文章浏览阅读1k次。uoyi-ui下载依赖报错npm ERR! cb() never called!_error running 'npm install in ruoyi-vue-master\ruoyi-ui': unresolved interpr

十大实用的办公工具网站,可以解决你日常100%的问题_办公网站导航-程序员宅基地

文章浏览阅读1.5k次。1.快捷键查询(这是个英文网站,推荐使用谷歌浏览器访问,再使用谷歌翻译)网站地址:https://usethekeyboard.com/Use The Keyboard是一个提供快捷键查询的网站的,你通过它可以查询苹果APP,windows系统,以及一些网站的快捷键操作,你知道熟练的掌握一些快捷键能够大大的提高工作或者学习的效率,当然,也不是特意的去记忆,而是在使用的过程中逐渐熟悉,前期就可以借助这种工具进行查询。2,MikuTools网站地址:https://miku.toolsM_办公网站导航