免責事項: このアプリは学習専用であり、商用利用は禁止されています。
以前に Android の基礎知識を復習しましたが、使いやすいアプリを開発しきれていないので、いくつかの小さなデモを書きました。そこで、自分の知識を統合するための簡単なアプリを書きたいと思いました。そして、SpecialDay が誕生しました。
1.需要分析
カウントダウン アプリとしての主な機能は、ユーザーがリマインダー イベントを追加し、アプリのホームページでイベントの残り日数をユーザーに表示することです。
また、毎年または毎月繰り返されるイベントもあり、ユーザーはイベントを追加するときに繰り返しの種類を選択できます。アプリは、繰り返しイベントのイベントを調整する必要があります。
ユーザーは、既存のイベントを変更および削除することもできます。
二、悟る
1. データベース設計
イベントには、少なくともタイトル、タイプ (後でカテゴリ別に表示するため)、日付、および繰り返しタイプの属性が必要なので、データベースの設計は次のようになります。
列名 | タイプ | ノート |
---|---|---|
ID | 整数 | 主キー、自動インクリメント |
タイトル | 文章 | タイトル |
タイプ | 整数 | タイプテーブルに対応するイベントタイプ |
開催日 | 日にち | 開催日 |
繰り返す | 整数 | 繰り返しの種類、0 - 繰り返しなし、1 - 毎年繰り返す、2 - 毎月繰り返す |
同時に、イベント タイプを格納するためのタイプ テーブルが必要です。
列名 | タイプ | ノート |
---|---|---|
ID | 整数 | 主キー |
名前 | 文章 | 型名 |
2. インターフェース設計
2.1 メインページ
メイン ページで表示する必要があるのは、タイトル バーとイベント リストです。
タイトル バーは RelativeLayout を使用して実装できます。次の 3 枚の画像は、インターネットから並べたものです。イベント一覧の表示には、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 の特定のアイテムも、レイアウトを実現するために 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 イベントアイテムアダプター
RecyclerView needs Adapter to fill data. ここで 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 メインアクティビティ
これは、アプリ全体のメイン インターフェイスです。彼がしなければならない作業は次のとおりです (実際には、ここで MVVM メソッドを使用する必要がありますが、MVVM フレームワークは深く理解されていないため、これらのことはアクティビティによって一時的に完了します)。
-
アプリを起動するときに、変更が必要なイベントの日付があるかどうかを判断します。たとえば、昨日のイベントが毎月繰り返される場合、最初に更新する必要があります。
-
次に、データベースに移動してデータを読み取り、データをアダプタに渡します。
-
コントロールにクリック イベントを追加する
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 を使用して実装されます。
ユーザー定義型を容易にするために、後続の型をデータベースから読み取る必要があります。しかし、ここでは冒頭で死ぬほど書かれています。2 つの Spinner のデータは、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>
次に、2 つの内部クラスを記述して、インターフェイス 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
}
}
3. まとめ
この記事は、アプリの実装プロセスに関するメモです。
実際、全体的にまだ多くの欠陥があります。たとえば、MVVM フレームワークを使用して、アクティビティのタスクを軽くすることができます。その後のイベントの追加と削除の操作では、タイトルをクリックしてカテゴリを選択し、イベントやその他の機能を表示します。
また、これは知識学習を統合するための単なるハンズオン アプリであるとも以前に述べました. ソース コードが必要な場合は、ここをクリックしてください: SpecialDay: シンプルな Android アプリケーション. さまざまな特別な日の記録に使用されます。