话不多说直接上图
- 从图中可知,分为
上下
两部分,上部分是传统春联和福字
,代表对大家的新年祝福
,下部分是主要
功能模块,包含红包金额、新年幸运签和是与不是
。 - 采用
Kotlin
语言进行编写,涉及到的技术有:ConstraintLayout
、Drawable
、
自定义View
、Android动画
、Viewpager2
、字体的设置
和传感器的使用
。创意来源
- 这个创意的来源,主要是年纪大了,过年肯定要给侄子侄女
发红包
,哈哈哈,这回金额可以他们自己摇出来
,具有互动和随机性
比较好玩,为新年增添一份乐趣
! - 新年
幸运签
是给大家的祝福
! - 大家肯定会有很多场景,会产生
选择困难
,所以是与不是
这个模块就是解决此类问题
添加的! - 新年也要
动起来
呀,刚好传统的摇签
可以用手机摇一摇
来模拟效果
,活动手腕一举两得
(真是个好点子啊)! -
安卓手机
的小伙伴可以下载安装包体验
一把,我是停不下来
!
正文开始啦
- 首先这个布局看起来挺简单的对吧,
LinearLayout
设置方向vertical
,中间在用一个LinearLayout
设置方向horizontal
。- 但是这就产生了一个问题,
布局嵌套
,所以这也是我为什么采用ConstraintLayout
来实现的原因,如下图,只用了一层
。
- 但是这就产生了一个问题,
ConstraintLayout使用
- 啰嗦两句,有的小伙伴可以没用过,可以参考下
- 在
ConstraintLayout
中的控件横竖两个方向
都至少要选择一个进行约束
,否则控件将在左上角进行摆放
。 -
top
指顶部bottom
指底部start
指左边end
指右边,上例子
- 在
- 这是
横批
的文字,可以看到,它的顶部
和父布局的顶部相约束
,左边
和父布局的左边相约束
,右边
和父布局的右边相约束
。-
横向居中
需要左右都加约束
,不需要的话,想让控件在哪个方向开始摆放
,就让它约束到该方向
,如横批靠顶部摆放
。
-
- 接下来我想让
上部分占百分之七十
,下部分占百分之三十
- 添加
Guideline控件
,上下分的话
设置orientation为horizontal
,想要左右分
改为vertical
即可。 -
layout_constraintGuide_percent
属性用来设置上或左
占多少,数值范围为0到1
。
- 添加
- 中间的
对联
和福
字,我打算福
字占百分之三十
,剩下的各占百分之十
,所以控件宽高都设置了0dp,即占满剩余空间,为它们设置横向
的权重
,1:3:1
。- 横向权重
app:layout_constraintHorizontal_weight
- 福字因为要宽高一一致,设置比例1:1即可
app:layout_constraintDimensionRatio="1:1"
- 需要注意:控件之间
必须相互依赖
才起作用。
- 横向权重
- 代码如下:
- 下面的
ViewPager2
和指示器
采用权重
,将剩余的空间按4:1
进行分配,这里权重
和LinearLayout
的用法一致。
Drawable使用
- 这里用的是
ShapeDrawable
- 他的好处就是可以为控件
添加背景
,减少
图片资源的使用
,从而降低包体积
的大小
。
- 他的好处就是可以为控件
- 上图中的
对联
和描边金线
均来自ShapeDrawable
,代码如下
自定义View
横批
- 不知道小伙伴有没有发现,
字体
不是系统自带的字体,我们要改变字体
,最简单的方法是继承TextView
,重写他的setTypeface
方法
- 新建如上图
目录
,放入我们需要的字体
。 -
使用这个字体
,并传给父类
。 - 在
布局文件
中使用
,代码在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()
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
来实现的,代码如下
指示器的添加
- 根据数量
动态创建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开启震动
//获取振动器管理者对象
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放大 |
- 有了以上说明接下来的
入场动画
,和出场动画
就更方便理解- 中心位置
从零到一
进行缩放
。
- 中心位置
- 退出是从
一到零
回退到中心位置
。
随机结果
- 这里并没有网络请求,采用
将答案写在本地
,随机抽取
展示。-
随机
的代码在Kotlin
中很简单如下
-
(answerList.indices).random()
- 本来想加数据库,支持人为输入的,后期慢慢实现吧。
最后
祝各位工程师,虎年大吉
,2022年心想事成
,想法几经改版
,差点流产
,还好最后坚持
做了出来。
写作不易,如果对你有一丢丢帮助或启发,感谢点赞支持,有问题也欢迎留言交流哦!