日期倒计时App——SpecialDay

声明:本App仅用于学习,禁止用于一切商业用途。

之前复习了一下Android的基础知识,自己又没有完全开发过一个能用的App,都是写一些小Demo。于是就想写一个简单的App来巩固自己的知识。于是SpecialDay就诞生了。

 

一、需求分析

作为一款倒计时的App,最主要的功能就是用户添加提醒事件,然后在App主页面向用户展示事件的剩余天数。

并且有一些事件属于是每年重复或每月重复的事件,用户在添加事件的时候可以选择重复类型。App需要调整重复事件的事件。

对于已有的事件用户也可以进行修改和删除。

二、实现

1.数据库设计

一个事件应该至少有标题,类型(方便后续可分类查看), 日期,重复类型的属性,所以数据库设计如下

列名类型注释
idinteger主键,自增
titletext标题
typeinteger事件类型,对应于type表格
event_datedate事件日期
repeatinteger重复类型,0-不重复,1-每年重复,2-每月重复

同时需要一个type表格存储事件类型

列名类型注释
idinteger主键
nametext类型名称

2.界面设计

2.1 主页面

在主页面里,需要展示的是标题栏和事件列表。

标题栏用一个RelativeLayout实现即可,这里排列的是三张图片,源于网络。而事件列表的展示我选用了RecyclerView。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <RelativeLayout
        android:id="@+id/main_menu"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        tools:ignore="UselessParent"
        android:gravity="center">

        <ImageView
            android:id="@+id/setting"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:src="@drawable/setting"
            android:layout_gravity="center_vertical"
            android:background="@color/white"/>

        <ImageView
            android:id="@+id/title"
            android:layout_width="150dp"
            android:layout_height="30dp"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="50dp"
            android:layout_marginStart="50dp"
            android:src="@drawable/title"
            android:layout_toEndOf="@id/setting"
            android:layout_toRightOf="@id/setting" />


        <ImageView
            android:id="@+id/add"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="50dp"
            android:layout_marginStart="50dp"
            android:src="@drawable/add"
            android:layout_toRightOf="@id/title"
            android:layout_toEndOf="@id/title"
            />
    </RelativeLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/main_menu">

    </androidx.recyclerview.widget.RecyclerView>

</RelativeLayout>

而RecyclerView具体的item也是用RelativeLayout和LinearLayout搭配使用实现布局。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:orientation="horizontal"
    android:paddingTop="10dp"
    android:layout_height="wrap_content">
​
​
    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="15dp"
        android:layout_marginLeft="15dp"
        android:layout_marginStart="15dp"
        android:layout_marginBottom="15dp"
        android:src="@drawable/calendar"/>
​
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="55dp"
        android:orientation="vertical">
​
​
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center_vertical"
            >
​
            <LinearLayout
                android:id="@+id/date_layout"
                android:layout_width="150dp"
                android:layout_height="match_parent"
                android:orientation="vertical">
​
                <TextView
                    android:id="@+id/tv_title"
                    android:layout_width="match_parent"
                    android:layout_height="30dp"
                    android:text="123"
                    android:textSize="20sp"
                    android:textColor="@color/black"
                    android:gravity="center_vertical"
                    android:paddingLeft="10dp"/>
​
                <TextView
                    android:id="@+id/tv_date"
                    android:layout_width="match_parent"
                    android:layout_height="20dp"
                    android:text="2021-01-04"
                    android:textSize="15sp"
                    android:paddingLeft="10dp"/>
​
​
            </LinearLayout>
​
​
            <TextView
                android:id="@+id/tv_countDown"
                android:layout_width="180dp"
                android:layout_height="match_parent"
                android:paddingRight="10dp"
                android:paddingTop="5dp"
                android:text="就在今天"
                android:textSize="20dp"
                android:gravity="end"
                android:paddingVertical="10dp"
                android:textColor="#3FBFBF"
                android:layout_toRightOf="@id/date_layout"
                android:layout_toEndOf="@id/date_layout"/>
​
​
        </RelativeLayout>
​
        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_marginTop="4dp"
            android:layout_marginLeft="5dp"
            android:background="#000000"
            android:layout_marginStart="5dp" />
​
    </LinearLayout>
​
​
</LinearLayout>

这样主页面的布局就完成了。

2.2 添加事件页面

先看一下效果。

 

同样是通过RelativeLayout和LinearLayout就能实现这个布局。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".AddEventActivity">
​
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:gravity="center"
        tools:ignore="UselessParent">
​
        <ImageView
            android:id="@+id/icon_return"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:src="@drawable/icon_return"
            android:layout_gravity="center_vertical"
            android:background="@color/white"/>
​
        <ImageView
            android:id="@+id/title"
            android:layout_width="150dp"
            android:layout_height="30dp"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="50dp"
            android:layout_marginStart="50dp"
            android:src="@drawable/title"
            android:layout_toRightOf="@id/icon_return"
            android:layout_toEndOf="@id/icon_return"/>
​
​
        <ImageView
            android:id="@+id/submit"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="50dp"
            android:layout_marginStart="50dp"
            android:src="@drawable/submit"
            android:layout_toRightOf="@id/title"
            android:layout_toEndOf="@id/title"
            />
    </RelativeLayout>
​
    <LinearLayout
​
        android:layout_width="match_parent"
        android:layout_height="50dp"
        >
​
        <TextView
            android:layout_width="80dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="10dp"
            android:text="标题"
            android:textSize="15sp"
            android:gravity="center"
            android:textColor="@color/black"/>
​
        <EditText
            android:id="@+id/add_et_title"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@null"
            android:layout_marginRight="40dp"
            android:layout_marginEnd="40dp"
            android:gravity="end"
            android:maxLines="1"
            android:maxLength="15"
            android:layout_marginTop="5dp"
            android:textSize="15sp"
            />
​
    </LinearLayout>
​
    <LinearLayout
​
        android:layout_width="match_parent"
        android:layout_height="50dp">
​
        <TextView
            android:layout_width="80dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="10dp"
            android:text="日期"
            android:textSize="15sp"
            android:gravity="center"
            android:textColor="@color/black"/>
​
        <TextView
            android:id="@+id/add_tv_date"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@null"
            android:layout_marginRight="40dp"
            android:layout_marginEnd="40dp"
            android:gravity="end"
            android:maxLines="1"
            android:textSize="15sp"
            android:paddingTop="8dp"
            android:text="2022-1-4"
            android:textColor="@color/black"
            />
​
    </LinearLayout>
​
    <LinearLayout
​
        android:layout_width="match_parent"
        android:layout_height="50dp">
​
        <TextView
            android:layout_width="80dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="10dp"
            android:text="分类"
            android:textSize="15sp"
            android:gravity="center"
            android:textColor="@color/black"/>
​
        <Spinner
            android:id="@+id/select_type"
            android:layout_width="140dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="140dp"
            android:layout_marginStart="140dp"
            android:layout_gravity="center_vertical"/>
​
    </LinearLayout>
​
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="50dp">
​
        <TextView
            android:layout_width="80dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="10dp"
            android:text="重复"
            android:textSize="15sp"
            android:gravity="center"
            android:textColor="@color/black"/>
​
        <Spinner
            android:id="@+id/select_repeat"
            android:layout_marginLeft="140dp"
            android:layout_width="140dp"
            android:layout_height="match_parent"
            android:layout_gravity="center_vertical"
            android:layout_marginStart="140dp" />
​
    </LinearLayout>
​
    
</LinearLayout>

3.具体实现

3.1 EventItemAdapter

RecyclerView需要Adapter去填充数据,这里Adapter主要做的事情就是把数据绑定到UI控件上去。

class EventItemAdapter(private val list: List<EventItem>) : RecyclerView.Adapter<EventItemAdapter.ItemViewHolder>() {
​
​
    inner class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val icon: ImageView = view.findViewById(R.id.iv_icon)
        val title: TextView = view.findViewById(R.id.tv_title)
        val date: TextView = view.findViewById(R.id.tv_date)
        val countDown: TextView = view.findViewById(R.id.tv_countDown)
    }
​
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.special_day_item, parent, false)
        return ItemViewHolder(view)
    }
​
    @SuppressLint("SetTextI18n", "SimpleDateFormat")
    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val event = list[position]
        holder.title.text = event.title
        holder.date.text = event.date
        val day = DateUtil.getCountDown(event.date)
        if (day > 0) {
            holder.countDown.text = "还有 $day 天"
            holder.countDown.setTextColor(android.graphics.Color.BLUE)
        } else if (day == 0) {
            holder.countDown.text = "就在今天!"
        } else if (day < -365){
            holder.countDown.text = "已经过了 ${abs(day/365)} 年"
            holder.countDown.setTextColor(android.graphics.Color.RED)
        } else {
            holder.countDown.text = "已经过了 ${abs(day)} 天"
            holder.countDown.setTextColor(android.graphics.Color.RED)
        }
    }
​
    override fun getItemCount(): Int = list.size
    
​
}

其中需要用到一个工具类去计算目标时间和系统时间相差的天数。由于用毫秒相减的方式对于需求来说并不适合,这里计算的方法是硬算有多少天。

   @SuppressLint("SimpleDateFormat")
    fun getCountDown(date: String): Int {
        val nowYear = getYear()
        val nowDay = getDayOfYear()
        val target = Calendar.getInstance()
        val ft = SimpleDateFormat("yyyy-MM-dd")
        val targetDate:Date?
        try {
            targetDate = ft.parse(date)!!
        }catch (e: ParseException) {
            e.printStackTrace()
            return 0
        }
        target.time = targetDate
        val targetYear = target.get(Calendar.YEAR)
        val targetDay = target.get(Calendar.DAY_OF_YEAR)
        if (nowYear == targetYear) {
            return targetDay - nowDay
        } else if (targetYear < nowYear) {
            return (targetYear - nowYear) * 365
        } else {
            var days = 0
            for (i in nowYear..targetYear) {
                if (i == nowYear) {
                    days += if(GregorianCalendar().isLeapYear(nowYear)) {
                        365 - nowDay
                    } else {
                        366 - nowDay
                    }
                } else if (i == targetYear) {
                    days += targetDay
                } else {
                    days += if(GregorianCalendar().isLeapYear(i)) {
                        365
                    } else {
                        366
                    }
                }
            }
            return days
        }
    }

3.2 MainActivity

这是整个app的主界面。他要做的工作如下(这里其实应该使用MVVM的方式,但是因为还没有深入了解过MVVM框架,这些事暂时由Activity完成):

  1. 在启动app的时候,判断一下是否有需要修改的事件日期。比如昨天的一个事件是每月重复,就需要先更新。

  2. 然后去数据库里读取数据,把数据传递给Adapter。

  3. 给控件添加点击事件

class MainActivity : AppCompatActivity() , View.OnClickListener{
​
    var width: Int = 0
    lateinit var myHelper: MyDatabaseHelper
    private val databaseName = "specialDay.db"
    private lateinit var db: SQLiteDatabase
​
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
        setContentView(R.layout.activity_main)
        setting.setOnClickListener(this)
        add.setOnClickListener(this)
        width = DeviceUtils.getScreenWidth(this)
        myHelper = MyDatabaseHelper(this, databaseName, 1)
        db = myHelper.writableDatabase
        checkRepeat()
        initContent()
    }
​
    override fun onClick(v: View) {
        when (v.id) {
            R.id.setting -> {
                val sql = "delete from event"
                db.execSQL(sql)
                Toast.makeText(this, "11111", Toast.LENGTH_SHORT).show()
                Log.d("MainActivity", width.toString())
            }
            R.id.add -> {
                val intent = Intent(this, AddEventActivity::class.java)
                startActivity(intent)
                finish()
            }
        }
    }
​
    private fun initContent() {
        val fakeData = getData()
        Log.d("MainActivity", fakeData.size.toString())
        val adapter = EventItemAdapter(fakeData)
        val manager = LinearLayoutManager(this)
        rv_content.layoutManager = manager
        rv_content.adapter = adapter
    }
​
    @SuppressLint("SimpleDateFormat")
    private fun getData(): List<EventItem>{
​
        val sql = "select * from event order by event_date"
        val cursor = db.rawQuery(sql, null)
        val expired = ArrayList<EventItem>()
        val unExpired = ArrayList<EventItem>()
        val ft = SimpleDateFormat("yyyy-MM-dd")
        if (cursor.moveToFirst()) {
            val calendar = Calendar.getInstance()
            do {
                val id = cursor.getInt(0)
                val title = cursor.getString(1)
                val type = cursor.getInt(2)
                val date = cursor.getString(3)
                val time = ft.parse(date)!!
                calendar.time = time
                val repeat = cursor.getInt(4)
                val year = calendar.get(Calendar.YEAR)
                val day = calendar.get(Calendar.DAY_OF_YEAR)
                if (year < DateUtil.getYear() ||
                        (year == DateUtil.getYear() && day < DateUtil.getDayOfYear())) {
                    expired.add(0, EventItem(id, title, type, date, repeat))
                } else {
                    unExpired.add(EventItem(id, title, type, date, repeat))
                }
            }while (cursor.moveToNext())
        }
        cursor.close()
        return unExpired + expired
    }
​
    @SuppressLint("SimpleDateFormat")
    private fun checkRepeat() {
        val nowDate = "${DateUtil.getYear()}-${DateUtil.getMonth()}-${DateUtil.getDayOfMonth()}"
        val sql = "select * from event where event_date < ? and repeat != 0"
        val db = myHelper.writableDatabase
        val cursor = db.rawQuery(sql, arrayOf(nowDate))
        if (cursor.moveToFirst()) {
            val calendar = Calendar.getInstance()
            val ft = SimpleDateFormat("yyyy-MM-dd")
            do {
                val id = cursor.getInt(0)
                val repeat = cursor.getInt(4)
                Log.d("MainActivity", repeat.toString())
                val date = cursor.getString(3)
                Log.d("MainActivity", "date:   $date")
                Log.d("MainActivity", "id:   $id")
                val time = ft.parse(date)!!
                calendar.time = time
                var year = calendar.get(Calendar.YEAR)
                var month = calendar.get(Calendar.MONTH) + 1
                val day = calendar.get(Calendar.DAY_OF_MONTH)
                if (repeat == 1) {
                    year += 1
                } else if (repeat == 2) {
                    month += 1
                    Log.d("MainActivity", "月份+1")
                }
                val newDate = "$year-$month-$day"
                Log.d("MainActivity", "newDate   $newDate")
                val updateSql = "update event set event_date = ? where id = ?"
                db.execSQL(updateSql, arrayOf(newDate, id))
            } while (cursor.moveToNext())
        }
        cursor.close()
    }

3.3 AddEventActivity

添加事件方面,有选择日期和选择类型、是否重复的选项。

3.3.1 选择日期

选择日期主要用到了DatePicker控件,然后利用AlertDialog弹出一个对话框的形式,效果如下:

 

首先,我们要先获取当前系统时间,用全局变量存储。

    private fun initDateTime() {
        year = DateUtil.getYear()
        month = DateUtil.getMonth()
        day = DateUtil.getDayOfMonth()
    }

然后添加点击事件弹出对话框。

 private fun chooseDate() {
        val dateStr = StringBuffer()
        val dialogView = View.inflate(this, R.layout.dialog_date, null)
        val datePicker: DatePicker = dialogView.findViewById(R.id.datePicker)!!
        datePicker.init(year, month - 1, day, this)
        AlertDialog.Builder(this).apply {
            setPositiveButton("设置") {dialog, _ ->
                mDate = dateStr.append(year.toString()).append("-")
                    .append(month.toString()).append("-").append(day.toString()).toString()
                add_tv_date.text = mDate
                dialog.dismiss()
            }
            setNegativeButton("取消") {dialog, _ ->
                dialog.dismiss()
            }
            setTitle("选择日期")
            create()
            setView(dialogView)
            show()
        }
    }

同时还需要实现接口DatePicker.OnDateChangedListener,重写方法onDateChanged

    override fun onDateChanged(view: DatePicker?, year: Int, monthOfYear: Int, dayOfMonth: Int) {
        this.year = year
        this.month = monthOfYear + 1
        this.day = dayOfMonth
    }

3.3.2 选择类型和是否重复

这两个都是用了下拉框Spinner实现。

后续类型应该是从数据库中读取,方便用户自定义类型。不过这里一开始是写死的。在arrays.xml里写死了两个Spinner的数据。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    
    <string-array name="repeat">
        <item>不重复</item>
        <item>每年</item>
        <item>每月</item>
    </string-array>
​
    <string-array name="type">
        <item>事件</item>
        <item>生日</item>
        <item>爱情</item>
        <item>生活</item>
        <item>节日</item>
        <item>娱乐</item>
        <item>学习</item>
        <item>工作</item>
    </string-array>
</resources>

然后写两个内部类实现接口AdapterView.OnItemSelectedListener来监听选择的内容

   inner class RepeatSelectListener: AdapterView.OnItemSelectedListener {
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            repeat = position
        }
​
        override fun onNothingSelected(parent: AdapterView<*>?) {
            repeat = 0
        }
    }
​
    inner class TypeSelectListener: AdapterView.OnItemSelectedListener {
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            mType = position
        }
​
        override fun onNothingSelected(parent: AdapterView<*>?) {
            mType = 0
        }
    }

三、总结

这篇文章是实现app过程的笔记。

其实整个还有很多的不足。比如可以采用MVVM框架,让Activity的任务不要那么繁重;后续添加又滑删除事件的操作,点击标题可以选择分类进行查看事件等功能。

前面也声明了这只是一个用来巩固知识学习的练手app,如果想要源码的小伙伴可以戳这里:SpecialDay: 一个简单的Android应用。用于记录各种特殊日子。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值