ConstraintLayout+ViewPager2打造《摇一摇新年幸运签》App

话不多说直接上图

image.png
  • 从图中可知,分为上下两部分,上部分是传统春联和福字,代表对大家的新年祝福,下部分是主要功能模块,包含红包金额、新年幸运签和是与不是
  • 采用Kotlin语言进行编写,涉及到的技术有:ConstraintLayoutDrawable
    自定义ViewAndroid动画Viewpager2字体的设置传感器的使用

    创意来源

  • 这个创意的来源,主要是年纪大了,过年肯定要给侄子侄女发红包,哈哈哈,这回金额可以他们自己摇出来,具有互动和随机性比较好玩,为新年增添一份乐趣
  • 新年幸运签是给大家的祝福
  • 大家肯定会有很多场景,会产生选择困难,所以是与不是这个模块就是解决此类问题添加的!
  • 新年也要动起来呀,刚好传统的摇签可以用手机摇一摇来模拟效果,活动手腕一举两得(真是个好点子啊)!
  • 安卓手机的小伙伴可以下载安装包 体验一把,我是停不下来

正文开始啦

  • 首先这个布局看起来挺简单的对吧,LinearLayout设置方向vertical,中间在用一个LinearLayout设置方向horizontal
    • 但是这就产生了一个问题,布局嵌套,所以这也是我为什么采用ConstraintLayout来实现的原因,如下图,只用了一层
image.png

ConstraintLayout使用

  • 啰嗦两句,有的小伙伴可以没用过,可以参考下
    • ConstraintLayout中的控件横竖两个方向至少要选择一个进行约束,否则控件将在左上角进行摆放
    • top指顶部bottom指底部start指左边end指右边,上例子
<com.android.springfestival.view.SpringTextView
    android:id="@+id/top"
    android:layout_width="wrap_content"
    android:layout_height="?actionBarSize"
    android:background="@drawable/shape_red_solid"
    android:gravity="center"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:text="金 虎 迎 福"
    android:textSize="24sp"
    android:textStyle="bold"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  • 这是横批的文字,可以看到,它的顶部父布局的顶部相约束左边父布局的左边相约束右边父布局的右边相约束
    • 横向居中需要左右都加约束,不需要的话,想让控件在哪个方向开始摆放,就让它约束到该方向,如横批靠顶部摆放
  • 接下来我想让上部分占百分之七十下部分占百分之三十
    • 添加Guideline控件上下分的话设置orientation为horizontal,想要左右分改为vertical即可。
    • layout_constraintGuide_percent属性用来设置上或左占多少,数值范围为0到1
      image.png
<androidx.constraintlayout.widget.Guideline
    android:id="@+id/guideline"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    app:layout_constraintGuide_percent="0.7" />
  • 中间的对联字,我打算字占百分之三十,剩下的各占百分之十,所以控件宽高都设置了0dp,即占满剩余空间,为它们设置横向权重1:3:1
    • 横向权重app:layout_constraintHorizontal_weight
    • 福字因为要宽高一一致,设置比例1:1即可app:layout_constraintDimensionRatio="1:1"
    • 需要注意:控件之间必须相互依赖才起作用。
  • 代码如下:
<com.android.springfestival.view.VerticalTextView
   android:id="@+id/left"
   android:layout_width="0dp"
   android:layout_height="0dp"
   android:layout_marginLeft="5dp"
   android:background="@drawable/shape_red_solid"
   android:ems="1"
   android:paddingTop="10dp"
   android:paddingBottom="10dp"
   android:text="迎春迎福迎富贵"
   android:textSize="24sp"
   android:textStyle="bold"
   app:layout_constraintHorizontal_weight="1"
   app:layout_constraintBottom_toTopOf="@id/guideline"
   app:layout_constraintEnd_toStartOf="@id/ling"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintTop_toBottomOf="@id/top" />

<com.android.springfestival.view.VerticalTextView
   android:id="@+id/right"
   android:layout_width="0dp"
   android:layout_height="0dp"
   android:layout_marginRight="5dp"
   android:background="@drawable/shape_red_solid"
   android:ems="1"
   android:paddingTop="10dp"
   android:paddingBottom="10dp"
   android:text="接财接福接平安"
   android:textSize="24sp"
   android:textStyle="bold"
   app:layout_constraintHorizontal_weight="1"
   app:layout_constraintBottom_toBottomOf="@id/guideline"
   app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintStart_toEndOf="@id/ling"
   app:layout_constraintTop_toBottomOf="@id/top" />

<com.android.springfestival.view.DiamondTextView
   android:id="@+id/ling"
   android:layout_width="0dp"
   app:layout_constraintDimensionRatio="1:1"
   app:layout_constraintHorizontal_weight="3"
   android:layout_height="0dp"
   android:layout_margin="5dp"
   android:autoSizeTextType="uniform"
   android:gravity="center"
   android:text="福"
   android:textStyle="bold"
   app:layout_constraintBottom_toTopOf="@+id/OptionVp"
   app:layout_constraintEnd_toStartOf="@id/right"
   app:layout_constraintStart_toEndOf="@id/left"
   app:layout_constraintTop_toBottomOf="@id/top"
   />
  • 下面的ViewPager2指示器采用权重,将剩余的空间按4:1进行分配,这里权重LinearLayout的用法一致。
<androidx.viewpager2.widget.ViewPager2
    android:id="@+id/OptionVp"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@id/llPointContainer"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/guideline"
    app:layout_constraintVertical_weight="4" />

<LinearLayout
    android:id="@+id/llPointContainer"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_marginTop="10dp"
    android:gravity="center_horizontal"
    android:orientation="horizontal"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/OptionVp"
    app:layout_constraintVertical_weight="1" />

Drawable使用

  • 这里用的是ShapeDrawable
    • 他的好处就是可以为控件添加背景减少图片资源的使用,从而降低包体积大小
image.png
  • 上图中的对联描边金线均来自ShapeDrawable,代码如下
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

 <stroke
     android:width="1dp"
     android:color="@color/colorGold" />
 <solid android:color="@color/colorRed" />
</shape>

自定义View

横批

  • 不知道小伙伴有没有发现,字体不是系统自带的字体,我们要改变字体,最简单的方法是继承TextView,重写他的setTypeface方法
image.png
  • 新建如上图目录,放入我们需要的字体
  • 使用这个字体,并传给父类
  • 布局文件使用,代码在ConstraintLayout章节中。
class SpringTextView(context: Context?, attrs: AttributeSet?) :
    AppCompatTextView(context, attrs) {
    //重写设置字体方法
    override fun setTypeface(tf: Typeface?) {
        super.setTypeface(Typeface.createFromAsset(context.assets, "fonts/hwxk.ttf"))
    }
}

对联

  • 相信大家都清楚,TextView可以用android:ems="1"达到竖直排列,但是紧贴在一起不能均分非常不美观,所以我们继续继承TextView,自定义竖直均分的效果
    • 这次我们重写onDraw方法,自己来进行文字的绘制
    • 高度绘制的关键点在于算出每个文字之间的空隙总的高度减去上下的padding和文字的宽度除以文字的个数减一
    • 宽度绘制(总的宽度减去文字的宽除以二
  • 代码如下:
override fun onDraw(canvas: Canvas) {
    paint.textSize = textSize
    paint.apply {
        typeface = Typeface.createFromAsset(context.assets,"fonts/hwxk.ttf")
    }
    var textLengthHeight = 0
    val r = Rect()
    val arr = IntArray(text.length)
    canvasLength = measuredHeight - paddingTop - paddingBottom
    if (!TextUtils.isEmpty(text) && text.length > 1) {
        var i = 0
        while (i < text.length) {
            paint.getTextBounds(text.substring(i, i + 1), 0, 1, r)
            textLengthHeight += (r.bottom - r.top)
            arr[i] = r.bottom - r.top
            i++
        }
        space = (canvasLength - textLengthHeight).toDouble() / (text.length - 1)
    }
    var arrlength = 0f
    var i = 0
    while (i < text.length) {
        arrlength += arr[i]
        if (i == 0) {
            canvas.drawText(
                text.substring(i, i + 1),
                ((width - r.right - r.left) / 2).toFloat(),
                (i * space + arrlength).toFloat() + paddingTop,
                paint
            )
        } else {
            canvas.drawText(
                text.substring(i, i + 1),
                ((width - r.right - r.left) / 2).toFloat(),
                (i * space + arrlength).toFloat(),
                paint
            )
        }
        i++
    }
}

福字

  • 菱形的TextView系统也没给咱,咋办呢,继续自定义!
    • 老规矩重写onDraw,获取宽高,取最短的,利用Path画一个出来,在为TextView设置背景即可。
    • 这里我画了两次,因为福字怎么能少了金边呢!
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   var min = min(width, height)
   var mPath = Path().apply {
       moveTo(0F, (min / 2).toFloat());
       lineTo((min / 2).toFloat(), 0F);
       lineTo(min.toFloat(), (min / 2).toFloat());
       lineTo((min / 2).toFloat(), min.toFloat());
       close();
   }
   val bmp = Bitmap.createBitmap(min, min, Bitmap.Config.ARGB_8888)
   val c = Canvas(bmp)
   c.drawPath(mPath, paint)
   c.drawPath(mPath, paintStock)
   setBackgroundDrawable(BitmapDrawable(resources, bmp))

ViewPager2

无限滑动的实现

  • 数据源的第一位add最后一张图
val newList = arrayListOf<String>()
newList.add(pic[pic.size-1])
  • 最后一位添加第一张图
for (item in pic) {
    newList.add(item)
}
newList.add(pic[0])
  • ViewPager2滑动到第0位最后一位时的处理分别如下
位置 处理
currentPosition == 0 setCurrentItem(adapter.itemCount - 2, false)
currentPosition == adapter.itemCount - 1 setCurrentItem(1, false)
  • ViewPager2添加滑动监听代码如下
    关键点在onPageScrollStateChanged方法
bannerVp.registerOnPageChangeCallback(object :
    ViewPager2.OnPageChangeCallback() {
    override fun onPageSelected(position: Int) {
        currentPosition = position
    }
    override fun onPageScrollStateChanged(state: Int) {
        //只有在空闲状态,才让自动滚动
        if (state == ViewPager2.SCROLL_STATE_IDLE) {
            if (currentPosition == 0) {
                bannerVp.setCurrentItem(adapter.itemCount - 2, false)
            } else if (currentPosition == adapter.itemCount - 1) {
                bannerVp.setCurrentItem(1, false)
            }
        }
    }
})

item的形状

  • 这里也是通过ShapeDrawable来实现的,代码如下
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <corners
        android:bottomRightRadius="50dp"
        android:topLeftRadius="50dp" />
    <stroke
        android:width="1dp"
        android:color="@color/colorGold" />
    <solid android:color="#F02A2A" />
</shape>

指示器的添加

  • 根据数量动态创建View,代码如下:
private fun initIndicator(){
    llPointContainer.removeAllViews()
    for (index in 1..size-2) {
        val view = View(this)
        val layoutParams = LinearLayout.LayoutParams(10.dp.toInt(), 10.dp.toInt())
        layoutParams.marginEnd = 8
        layoutParams.marginStart = 8
        view.layoutParams = layoutParams
        llPointContainer.addView(view)
    }
}
  • 滑动的时候更新指示器背景
    • ViewPager2的滑动监听的onPageSelected方法中调用如下方法即可
      • 记得做如下判断
if (position <= llPointContainer.childCount) updateIndicator(position)
private fun updateIndicator(position: Int){
    llPointContainer.run {
        for (index in 1..childCount) {
            getChildAt(index - 1).background = resources.getDrawable(R.drawable.circlered)
        }
        if (position > 0) {
            getChildAt(position - 1).background = resources.getDrawable(R.drawable.circlegold)
        }
    }
}
  • 这里的形状也是通过ShapeDrawable实现的。

ViewPager2一屏多页效果

  • 这里和ViewPager一屏多页有很大区别ViewPager采用为给自身设置margin并设置clipChildren属性为false
  • ViewPager2则是通过给RecyclerView设置PaddingPageTransformer的方式来实现
OptionVp.apply {
 offscreenPageLimit=1
 val recyclerView= getChildAt(0) as RecyclerView
 recyclerView.apply {
     val padding = resources.getDimensionPixelOffset(R.dimen.common_line_height) +
             resources.getDimensionPixelOffset(R.dimen.common_line_height)
     // setting padding on inner RecyclerView puts overscroll effect in the right place
     setPadding(padding, 0, padding, 0)
     clipToPadding = false
 }
}

ViewPager2滑动缩放

  • 说到这就要讲一下PageTransformer了,它可以用来设置页面动画,还可以设置页面间距间距和动画都要的话就要用到CompositePageTransformer了。
  • 我这里如上一条,设置了页面间距并且用到了缩放效果,那么来看一下具体代码
val compositePageTransformer = CompositePageTransformer()
compositePageTransformer.addTransformer(ScaleInTransformer())
compositePageTransformer.addTransformer(
    MarginPageTransformer(
        resources.getDimension(R.dimen.common_margin_middle).toInt()
    )
)
OptionVp.setPageTransformer(compositePageTransformer)
  • 是不是很香,快用起来吧

传感器

  • Android中有很多传感器,这里我们用到的是加速度传感器,使用步骤如下:
    • 获取传感器管理者对象
    • 获取加速度传感器对象
    • 注册传感器(onCreate中调用)
    • 解除传感器(onDestory中调用)
sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
sensor = sensorManager!!.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
sensorManager!!.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL)
sensorManager!!.unregisterListener(this)
  • 注册了监听器之后在onSensorChanged方法中做业务的判断(这里采用获取event.values大于15),符合业务条件就调用震动弹出提示框
override fun onSensorChanged(event: SensorEvent) {
        /* 当传感器数值发生改变时调用的函数*/
        val values: FloatArray = event.values
        val x = values[0]
        val y = values[1]
        val z = values[2]
        val minValue = 15
        if(!isShake) {
            if (Math.abs(x) > minValue || Math.abs(y) > minValue || Math.abs(z) > minValue) {
                //开始震动
                isShake = true
                val pattern = longArrayOf(300, 500)
                vibrator!!.vibrate(pattern, -1)
                //开始动画效果
                MyDialog(this)
                    .showDialog(currentPosition - 1)

            }
        }
    }

震动的实现

  • 震动需要在manifest文件中申请权限
  • 获取振动器管理者对象
  • 调用vibrate开启震动
<!-- 振动器使用权限-->
<uses-permission android:name="android.permission.VIBRATE"/>
//获取振动器管理者对象
vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator
//开启震动
val pattern = longArrayOf(300, 500)
vibrator!!.vibrate(pattern, -1)

Android动画

  • 这里我们使用的是View动画Dialog添加入场和退场动画
    *View动画有如平移、缩放、旋转和透明度,这里使用了缩放
标签 含义
interpolator 指定动画插入器,常见的有加速减速插入器accelerate_decelerate_interpolator,加速插入器elerate_interpolator,减速插入器decelerate_interpolator。
pivotX 横向动画起始位置,相对于屏幕的百分比,50%表示动画从屏幕中间开始
pivotY 纵向动画起始位置,相对于屏幕的百分比,50%表示动画从屏幕中间开始
fromXScale 横向动画开始前的缩放,0.0为不显示,1.0为正常大小
toXScale 横向动画最终缩放的倍数,1.0为正常大小,大于1.0放大
fromYScale 纵向动画开始前的缩放,0.0为不显示,1.0为正常大小
toYScale 纵向动画最终缩放的倍数,1.0为正常大小,大于1.0放大
  • 有了以上说明接下来的入场动画,和出场动画就更方便理解
    • 中心位置从零到一进行缩放
<!-- 弹出时动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <scale
        android:interpolator="@android:anim/accelerate_interpolator"
        android:fromXScale="0.0"
        android:toXScale="1.0"
        android:fromYScale="0.0"
        android:toYScale="1.0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:fillAfter="false"
        android:duration="400"/>
</set>
<!-- 退出时动画效果 -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <scale
        android:interpolator="@android:anim/accelerate_interpolator"
        android:fromXScale="1.0"
        android:toXScale="0.0"
        android:fromYScale="1.0"
        android:toYScale="0.0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:fillAfter="false"
        android:duration="400"/>
</set>
  • 退出是从一到零回退到中心位置

随机结果

  • 这里并没有网络请求,采用将答案写在本地随机抽取展示。
    • 随机的代码在Kotlin中很简单如下
(answerList.indices).random()
  • 本来想加数据库,支持人为输入的,后期慢慢实现吧。

最后

祝各位工程师,虎年大吉,2022年心想事成,想法几经改版,差点流产,还好最后坚持做了出来。

写作不易,如果对你有一丢丢帮助或启发,感谢点赞支持,有问题也欢迎留言交流哦!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容