安卓自定义日历


android kotlin

自定义日历并不是自定义CalendarView,自定义CalendarView需要用到Canvas。这里都是使用最基本的布局元素实现的布局

效果图:

  • 使用布局文件创建自定义日历的布局

    • 使用GridView实现日历每天的网格

    • 两个按钮切换日期

    • 显示当前月份

  • 创建日历中每天的布局

  • 实现日期计算算法(计算需要空出几个格子,每天的基本信息之类的)

    • Adapter适配器,用于GridView

首先是日历的布局文件内容编写

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

<!--    Calendar Title    -->
    <RelativeLayout
        android:id="@+id/relativeLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <ImageButton
            android:id="@+id/btnPrevDate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentStart="true"
            android:layout_marginStart="20dp"
            android:layout_marginTop="20dp"
            android:src="@drawable/arrow_left2" />

        <TextView
            android:id="@+id/tvCalendarTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="35dp"
            android:layout_toStartOf="@+id/btnNextDate"
            android:layout_toEndOf="@+id/btnPrevDate"
            android:text="2020-08"
            android:textAlignment="center"
            android:textSize="24sp" />

        <ImageButton
            android:id="@+id/btnNextDate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_marginTop="20dp"
            android:layout_marginEnd="20dp"
            android:src="@drawable/arrow_right2" />
    </RelativeLayout>

<!--    Calendar Week Name    -->
    <LinearLayout
        android:id="@+id/linearLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/relativeLayout">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center_horizontal"
            android:text="MON"
            android:textSize="18sp" />

	<!--  周标题……  -->

    </LinearLayout>

<!--    Calendar Days-->
    <GridView
        android:id="@+id/customCalendarDays"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:numColumns="7"
        android:gravity="center"
        android:verticalSpacing="5dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/linearLayout">

    </GridView>

</androidx.constraintlayout.widget.ConstraintLayout>

没什么好说的,就是基本的一些布局配置

然后是日历Activity代码部分

class CustomCalendarActivity : AppCompatActivity() {

    // 需要用到的变量
    private lateinit var binding: CalendarLayoutBinding
    private lateinit var calendarAdapter: CalendarAdapter
    private var currentDate: Calendar = Calendar.getInstance()
    private val sdf = SimpleDateFormat("yyyy-MM", Locale.CHINESE)
    private val random: Random = Random()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = CalendarLayoutBinding.inflate(layoutInflater)
        setContentView(binding.root)

        title = "Custom Calendar"

        supportActionBar?.apply {
            setDisplayHomeAsUpEnabled(true)
        }
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (item.itemId == android.R.id.home) {
            finish()
            return true
        }
        return super.onOptionsItemSelected(item)
    }

    private fun updateMonth() {
	// 更新日历
    }
}

我们需要通过ArrayAdapter实现日历GridView展示日期,那么接下来就来重写一下ArrayAdapter

首先是实体类用来封装渲染日期需要用到的信息

class CalendarDay (val date: Calendar?, val price: Double)

然后是渲染日期单元格需要的布局文件

<!--calendar_empty_day.xml-->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="50dp"
    android:layout_height="50dp">
<!--空单元格-->
</androidx.constraintlayout.widget.ConstraintLayout>

<!--calendar_day_layout.xml-->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="50dp"
    android:layout_height="50dp"
    android:background="#DDDDDD">
<!--日期单元格显示的信息-->
    <TextView
        android:id="@+id/tvDayName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="1"
        android:textSize="12sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tvDayPrice"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="$XX.XX"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

最后是Adapter具体的代码

class CalendarAdapter(context: Context, private var data: MutableList<CalendarDay>) :
    ArrayAdapter<CalendarDay>(context, R.layout.calendar_day_layout, data) {
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val itemView: View
        val day = getItem(position)
        val inflater = LayoutInflater.from(context)
        // 如果是空就使用空白布局文件实现填充空单元格
        if (day?.date == null) {
            itemView = inflater.inflate(R.layout.calendar_empty_day, parent, false)
            return itemView
        }
        // 防止安卓内置的优化机制造成渲染多次同样的内容
        itemView = inflater.inflate(R.layout.calendar_day_layout, parent, false)
        itemView?.findViewById<TextView>(R.id.tvDayName)?.text = "${day.date.get(Calendar.DAY_OF_MONTH)}"
        itemView?.findViewById<TextView>(R.id.tvDayPrice)?.text = "$${day.price}"

        return itemView
    }

    fun updateDate(data: MutableList<CalendarDay>) {
        // 更新日历
        this.data.clear()
        this.data.addAll(data)
        notifyDataSetChanged()
    }
}

这里提到了安卓优化机制 简单来说就是有一个视图复用的问题,后面也许会专门写一下这个

然后就是修改一下之前写的Activity,实现日期更新之类的功能

  • 基本配置

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = CalendarLayoutBinding.inflate(layoutInflater)
            setContentView(binding.root)
    
            calendarAdapter = CalendarAdapter(baseContext, mutableListOf())
            updateMonth()
            title = "Custom Calendar"
    
            supportActionBar?.apply {
                setDisplayHomeAsUpEnabled(true)
            }
    
            binding.btnPrevDate.setOnClickListener {
                currentDate.add(Calendar.MONTH, -1)
                updateMonth()
            }
            binding.btnNextDate.setOnClickListener {
                currentDate.add(Calendar.MONTH, 1)
                updateMonth()
            }
            binding.customCalendarDays.apply {
                adapter = calendarAdapter
            }
        }
    
  • 切换日期逻辑

        private fun updateMonth() {
            val tempDate: Calendar = currentDate.clone() as Calendar
            tempDate.set(Calendar.DAY_OF_MONTH, 1)
    
            // 更新标题
            binding.tvCalendarTitle.text = sdf.format(tempDate.time)
    
            // 更新月份中的每一天
            // 填充空日期
            val dates = mutableListOf<CalendarDay>()
            var beforeEmptyCount = tempDate.get(Calendar.DAY_OF_WEEK)
            beforeEmptyCount -= 1
    
            if (beforeEmptyCount == 0) beforeEmptyCount = 7
            for (i in 2..beforeEmptyCount) {
                dates.add(CalendarDay(null, 0.0))
            }
    
            do {
                val price = (random.nextDouble() * 100).roundToInt() / 1.00
                dates.add(CalendarDay(tempDate.clone() as Calendar, price))
                tempDate.add(Calendar.DAY_OF_MONTH, 1)
            } while (tempDate.get(Calendar.MONTH) == currentDate.get(Calendar.MONTH))
            calendarAdapter.updateDate(dates)
        }
    

最后就可以实现自定义日历

分享这篇文章