class SunriseSunsetView : View {
private val DEFAULT_SUN_STROKE_WIDTH_PX = 4f
/**
* 当前日出日落比率, mRatio < 0: 未日出, mRatio > 1 已日落
*/
private var mRatio = 0f
private var mTrackPaint: Paint? = null // 绘制半圆轨迹的Paint
private var mTrackColor = Color.WHITE // 轨迹的颜色
private var mTrackWidth = 4 // 轨迹的宽度
// 轨迹的PathEffect
private var mTrackPathEffect: PathEffect = DashPathEffect(floatArrayOf(15f, 15f), 1f)
// 轨迹圆的半径
private var mTrackRadius = 0f
private var mSunPaint: Paint? = null // 绘制太阳的Paint
private var mSunColor = Color.parseColor("#FFCD3C")// 太阳颜色 // 太阳颜色
private var mSunRadius = 50f// 太阳半径
private var mSunPaintStyle = Paint.Style.FILL // 太阳Paint样式,默认FILL
private val MINIMAL_TRACK_RADIUS_PX = 300 // 半圆轨迹最小半径
/**
* 日出时间
*/
private var mSunriseTime: SunTime? = null
/**
* 日落时间
*/
private var mSunsetTime: SunTime? = null
/**
* 绘图区域
*/
private val mBoardRectF = RectF()
//太阳高度和整体view高度除
private var mViewAndSun = 2
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context, attrs, defStyleAttr
) {
val a: TypedArray =
context.obtainStyledAttributes(attrs, R.styleable.SunriseSunsetView, defStyleAttr, 0)
mTrackColor = a.getColor(R.styleable.SunriseSunsetView_ssv_track_color, Color.WHITE)
mTrackWidth = a.getDimensionPixelSize(R.styleable.SunriseSunsetView_ssv_track_width, 4)
mSunColor = a.getColor(R.styleable.SunriseSunsetView_ssv_sun_color, Color.YELLOW)
mSunRadius =
a.getDimensionPixelSize(R.styleable.SunriseSunsetView_ssv_sun_radius, 20).toFloat()
a.recycle()
init()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val paddingRight = this.paddingRight
val paddingLeft = this.paddingLeft
val paddingTop = this.paddingTop
val paddingBottom = this.paddingBottom
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
var widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
if (widthSpecMode == MeasureSpec.AT_MOST) {
widthSpecSize =
paddingLeft + paddingRight + MINIMAL_TRACK_RADIUS_PX * 2 + mSunRadius.toInt() * 2
}
mTrackRadius = 1.0f * (widthSpecSize - paddingLeft - paddingRight - 2 * mSunRadius) / 2
val expectedHeight =
(mTrackRadius / mViewAndSun + mSunRadius + paddingBottom + paddingTop).toInt()
mBoardRectF[paddingLeft + mSunRadius, paddingTop + mSunRadius, widthSpecSize - paddingRight - mSunRadius] =
(expectedHeight - paddingBottom).toFloat()
setMeasuredDimension(widthSpecSize, expectedHeight)
}
private fun init() {
// 初始化半圆轨迹的画笔
mTrackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
color = mTrackColor
strokeWidth = mTrackWidth.toFloat()
pathEffect = mTrackPathEffect
}
prepareTrackPaint()
// 初始化太阳的Paint
mSunPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = mSunColor
strokeWidth = DEFAULT_SUN_STROKE_WIDTH_PX
style = mSunPaintStyle
}
prepareSunPaint()
}
// 半圆轨迹的画笔
private fun prepareTrackPaint() {
mTrackPaint!!.color = mTrackColor
mTrackPaint!!.strokeWidth = mTrackWidth.toFloat()
mTrackPaint!!.pathEffect = null;//半圆虚线
}
// 太阳的画笔
private fun prepareSunPaint() {
mSunPaint!!.color = mSunColor
mSunPaint!!.strokeWidth = DEFAULT_SUN_STROKE_WIDTH_PX
mSunPaint!!.style = mSunPaintStyle
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawSunTrack(canvas)
drawSun(canvas)
drawSubLine(canvas)
}
// 绘制太阳轨道(半圆)
private fun drawSunTrack(canvas: Canvas) {
prepareTrackPaint()
canvas.save()
val rectF = RectF(
mBoardRectF.left,
mBoardRectF.top,
mBoardRectF.right,
mBoardRectF.bottom + mBoardRectF.height()
)
val startAngle = 180f
val sweepAngle = 180f
val ratioAngle = sweepAngle * mRatio // 根据比率计算角度
// 绘制左侧部分
val leftColor = Color.parseColor("#FFCD3C") // 橙色
val leftPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
color = leftColor
strokeWidth = mTrackWidth.toFloat()
}
canvas.drawArc(rectF, startAngle, ratioAngle, false, leftPaint)
// 绘制右侧部分
val rightColor = Color.parseColor("#E8E8E8")// 灰色
val rightPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
color = rightColor
strokeWidth = mTrackWidth.toFloat()
}
canvas.drawArc(rectF, startAngle + ratioAngle, sweepAngle - ratioAngle, false, rightPaint)
canvas.restore()
}
private var mSunCenterX = 0f// 太阳中心点X坐标
private var mSunCenterY = 0f // 太阳中心点Y坐标
// 绘制太阳
private fun drawSun(canvas: Canvas) {
prepareSunPaint()
canvas.save()
val curPointX =
mBoardRectF.left + mTrackRadius - mTrackRadius * cos(Math.PI * mRatio).toFloat()
val curPointY =
mBoardRectF.bottom - mTrackRadius / mViewAndSun * sin(Math.PI * mRatio).toFloat()
canvas.drawCircle(curPointX, curPointY, mSunRadius, mSunPaint!!)
canvas.restore()
mSunCenterX = curPointX
mSunCenterY = curPointY
}
// 绘制太阳周围线条
private fun drawSubLine(canvas: Canvas) {
mSunPaint?.style = mSunPaintStyle
mSunPaint?.color = mSunColor
mSunPaint?.strokeWidth = 8f // 设置线条宽度
mSunPaint?.strokeCap = Paint.Cap.ROUND // 设置线条端点为圆滑
val angleDelta = 360f / 8
val distanceToSun = mSunRadius + 8f//太阳周围线条到太阳的距离
val lineLength = 8f//太阳周围线条的长度
for (i in 0 until 8) {
val angle = 180f + angleDelta * i
val radian = Math.toRadians(angle.toDouble())
val startX = mSunCenterX + distanceToSun * cos(radian).toFloat()
val startY = mSunCenterY + distanceToSun * sin(radian).toFloat()
val endX = startX + lineLength * cos(radian).toFloat()
val endY = startY + lineLength * sin(radian).toFloat()
canvas.drawLine(startX, startY, endX, endY, mSunPaint!!)
}
}
fun startAnimate(sunriseTime: SunTime, sunsetTime: SunTime) {
// 检查日出和日落时间是否为空
if (sunriseTime == null || sunsetTime == null) {
return
}
// 计算日出和日落的分钟数
val sunriseMinutes = sunriseTime.transformToMinutes()
val sunsetMinutes = sunsetTime.transformToMinutes()
// 获取当前时间的分钟数
val calendar = Calendar.getInstance(Locale.getDefault())
val currentTime =
calendar[Calendar.HOUR_OF_DAY] * SunTime.MINUTES_PER_HOUR + calendar[Calendar.MINUTE]
// 计算比率并确保在 [0, 1] 范围内
var ratio = 1.0f * (currentTime - sunriseMinutes) / (sunsetMinutes - sunriseMinutes)
ratio = ratio.coerceIn(0f, 1.0f)
// 设置 mRatio 的值并刷新视图
mRatio = ratio
invalidate()
}
}
class SunTime(val hour: Int, val minute: Int) {
companion object {
const val MINUTES_PER_HOUR = 60
}
constructor(hourStr: String, minuteStr: String) : this(
hourStr.toIntOrNull() ?: 0,
minuteStr.toIntOrNull() ?: 0
)
fun transformToMinutes(): Int {
return hour * MINUTES_PER_HOUR + minute
}
override fun toString(): String {
val formattedHour = hour.toString().padStart(2, '0')
val formattedMinute = minute.toString().padStart(2, '0')
return "$formattedHour:$formattedMinute"
}
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="red">#F44336</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="grey">#616161</color>
<color name="pink">#F50057</color>
<color name="purple">#D500F9</color>
<color name="indigo">#3D5AFE</color>
<color name="teal">#00BFA5</color>
<color name="green">#00E676</color>
<color name="amber">#FFC400</color>
<color name="brown">#3E2723</color>
<color name="lightGreen">#809CCC65</color>
<color name="Orange_sun">#FFCD3C</color>
<declare-styleable name="progressView">
<attr name="outerColor" format="color" />
<attr name="innerColor" format="color" />
<attr name="borderWidth" format="dimension" />
<attr name="stepTextSize" format="dimension" />
<attr name="stepTextColor" format="color" />
</declare-styleable>
</resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MyVIewLine" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SunAnimationView">
<attr name="sun_circle_color" format="color"></attr>
<attr name="sun_font_color" format="color"></attr>
<attr name="sun_font_size" format="dimension"></attr>
<attr name="sun_circle_radius" format="integer"></attr>
</declare-styleable>
<declare-styleable name="SunriseSunsetView">
<!-- 轨道颜色 -->
<attr name="ssv_track_color" format="color|reference" />
<!-- 轨道宽度 -->
<attr name="ssv_track_width" format="dimension" />
<!--太阳半径 -->
<attr name="ssv_sun_radius" format="dimension" />
<!-- 太阳的颜色 -->
<attr name="ssv_sun_color" format="color|reference" />
</declare-styleable>
</resources>
<com.example.myviewline.view.SunriseSunsetView
android:id="@+id/sun"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:paddingTop="10dp"
app:ssv_sun_color="@color/Orange_sun"
app:ssv_sun_radius="10dp"
app:ssv_track_width="3dp" />
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initSun()
}
private fun initSun() {
val (sunriseHour, sunriseMinute) = "08:19".split(":")//日出时间
val (sunsetHour, sunsetMinute) = "19:55".split(":")//日落时间
binding.sun.startAnimate(
SunTime(sunriseHour, sunriseMinute), SunTime(sunsetHour, sunsetMinute)
)
}
}
效果图: