Date Countdown App——SpecialDay

Disclaimer: This App is for learning only, and any commercial use is prohibited.

I reviewed the basic knowledge of Android before, and I haven't fully developed a usable App, so I wrote some small demos. So I wanted to write a simple App to consolidate my knowledge. And so SpecialDay was born.

 

1. Demand Analysis

As a countdown app, the main function is for the user to add a reminder event, and then display the remaining days of the event to the user on the App homepage.

And there are some events that are repeated every year or every month, and the user can choose the recurrence type when adding an event. App needs to adjust events for repeating events.

Users can also modify and delete existing events.

Two, realize

1. Database design

An event should have at least title, type (for subsequent viewing by category), date, and repeat type attributes, so the database design is as follows

column name type note
id integer primary key, auto increment
title text title
type integer Event type, corresponding to the type table
event_date date event date
repeat integer Repeat type, 0-no repeat, 1-repeat every year, 2-repeat every month

At the same time, a type table is required to store the event type

column name type note
id integer primary key
name text type name

2. Interface design

2.1 Main page

On the main page, what needs to be displayed is the title bar and event list.

The title bar can be implemented with a RelativeLayout. Here are three pictures arranged from the Internet . For the display of the event list, I chose 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>

The specific item of RecyclerView is also used in conjunction with RelativeLayout and LinearLayout to achieve layout.

<?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>

This completes the layout of the main page.

2.2 Add event page

Let's take a look at the effect first.

 

This layout can also be achieved through RelativeLayout and 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. Specific implementation

3.1 EventItemAdapter

RecyclerView needs Adapter to fill data. The main thing that Adapter does here is to bind data to UI controls.

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
    
​
}

Among them, a tool class is needed to calculate the number of days between the target time and the system time. Since the method of subtracting milliseconds is not suitable for the demand, the calculation method here is to calculate the number of days.

   @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

This is the main interface of the entire app. The work he has to do is as follows (in fact, the MVVM method should be used here, but because the MVVM framework has not been deeply understood, these things are temporarily completed by Activity):

  1. When starting the app, determine whether there is an event date that needs to be modified. For example, if an event yesterday is repeated every month, it needs to be updated first.

  2. Then go to the database to read the data and pass the data to the Adapter.

  3. Add a click event to the control

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

In terms of adding an event, there are options for selecting a date, selecting a type, and whether to repeat.

3.3.1 Select date

The date selection mainly uses the DatePicker control, and then uses the AlertDialog to pop up a dialog box, the effect is as follows:

 

First of all, we need to obtain the current system time and store it in a global variable.

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

Then add a click event to popup the dialog.

 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()
        }
    }

At the same time, it is also necessary to implement the interface DatePicker.OnDateChangedListener and rewrite the method 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 Selection type and whether to repeat

Both of these are implemented using the drop-down box Spinner.

Subsequent types should be read from the database to facilitate user-defined types. But here is written to death at the beginning. The data of two Spinners are written in arrays.xml.

<?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>

Then write two internal classes to implement the interface AdapterView.OnItemSelectedListener to monitor the selected content

   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
        }
    }

3. Summary

This article is a note on the process of implementing the app.

In fact, there are still many deficiencies in the whole. For example, the MVVM framework can be used to make the tasks of the Activity less onerous; in the subsequent operations of adding and deleting events, click on the title to select categories to view events and other functions.

It was also stated earlier that this is just a hands-on app for consolidating knowledge and learning. If you want the source code, you can click here: SpecialDay: A simple Android application. Used to record various special days.

Guess you like

Origin blog.csdn.net/qq_43478882/article/details/122508998