话不多说直接上图

image.png
- 从图中可知,分为
上下两部分,上部分是传统春联和福字,代表对大家的新年祝福,下部分是主要功能模块,包含红包金额、新年幸运签和是与不是。 - 采用
Kotlin语言进行编写,涉及到的技术有:ConstraintLayout、Drawable、
自定义View、Android动画、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
- 之前写过一篇ViewPager2打造Banner轮播图的文章,这里在简单啰嗦两句,可能有的小伙伴没看之前的文章。
无限滑动的实现
- 数据源的第一位
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设置Padding和PageTransformer的方式来实现
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年心想事成,想法几经改版,差点流产,还好最后坚持做了出来。
写作不易,如果对你有一丢丢帮助或启发,感谢点赞支持,有问题也欢迎留言交流哦!
