目录
源码:https://github.com/Alex-Shen1121/SZU_Learning_Resource/tree/main/%E8%AE%A1%E7%AE%97%E6%9C%BA%E4%B8%8E%E8%BD%AF%E4%BB%B6%E5%AD%A6%E9%99%A2/%E7%A7%BB%E5%8A%A8%E8%AE%BE%E5%A4%87%E4%BA%A4%E4%BA%92%E5%BA%94%E7%94%A8/%E5%AE%9E%E9%AA%8C/%E5%AE%9E%E9%AA%8C3-%E6%88%91%E7%9A%84%E6%A0%A1%E5%9B%AD
一、实验目的与内容:
目的:掌握安卓中活动的编写、自定义用户界面的开发、碎片开发、广播机制以及数据持久化技术等;并能通过对课堂知识进行扩展来完善该界面,并使界面尽量美观。
内容要求:
-
请尽量模拟如下深大校园主页的功能,参考:
https://www1.szu.edu.cn/
-
具体要求:
- 该实现的界面在某些地方应体现出如下功能:
a. 界面能对平板与手机平台进行自适应(参考第4章碎片);
b. 能对用户身份有强制下线的功能,比如网络中断,登录界面强行退出并显示提示错误的界面;
c. 界面某些地方体现数据持久化的技术,如文件数据的读取、存储的多种实现方式,并简单阐述几种实现方式具体的适用场景;
d. 界面要比较工整,没必要实现参考界面上的所有子项,能保证自己的界面实现能有扩展到参考界面的能力即可。 - 功能并不局限于上面的要求,可以根据自己的理解设计一些新的功能,并在报告文档中进行详细的阐述,作为报告的亮点;
3)APP的布局尽快模仿参考界面,如果有较大的困难,可以只实现出右半边部分的界面,并尽量按上面要求进行完善; - 对于某一种功能,可以在不同的子项处采用多种实现方式,并比较这些实现方式的不同及优劣势。
- 该实现的界面在某些地方应体现出如下功能:
-
参考:尽量多的应用参考书《第一行代码 Android》第二版第2章活动、第3章UI开发第4章碎片、第5章广播机制与第6章数据持久化技术的各个知识点。
注意:
- 实验报告中需要有功能的描述、实验结果的截屏图像及详细说明;
- 也欢迎采用其它章节的知识点完成本次实验报告,如果实现的功能言之合理,会考虑酌情加分。
二、实验过程和代码与结果
“我的校园”APP的构建过程及结果
注:测试环境为华为nova5 pro 6.39寸手机与虚拟机Nexus 9 8.86寸平板。
界面展示:
主页:
(平板)(手机)
学生界面:
管理员界面:
构建过程:
具体项目构建过程可以参考github
URL:https://github.com/Alex-Shen1121/SZU_Website_Android
- 编写Activity基类与ActivityCollector为实现强制下线功能打下基础
- 设计登陆界面布局,编写账号登录逻辑
- 设计编写管理员主界面,完成用户信息展示
- 实现强制下线功能
- 设计管理员修改界面,完成文件的修改逻辑
- 设计学生界面,并完成左右Fragment的平板手机自适应功能
- 编写学生界面各个布局的内容填充
- 实现网页的跳转
- 修复部分bug
具体各个部分的关键代码会在下一个部分进行展示。
请详细说明“我的校园”APP的功能、出现的关键问题及解决方案
“我的校园”APP的功能亮点介绍及代码分析:
(以下截图将以手机页面进行展示,平板上有基本一致的效果)
本次APP提供两个默认账号用于登录。
学生账户用户名:student 学生账户密码:123456
管理员账户用户名:admin 管理员账户密码:123456
①账号登陆匹配
主要技术参考章节:7.2文件存储,3 Activity
思路:
- 通过文件读写的方式获取APP的默认账户,后期可以通过连接云端服务器。
- 将正确的用户名密码以键值对的方式保存。
- 与用户输入的用户名密码进行匹配,如果完全匹配则进入相对应界面,否则弹出错误提示,并删去密码,重新输入(更加符合使用习惯)。
具体核心代码展示:
- 添加默认账号文件
private fun addDefaultAccount() {
//判断文件是否存在
val file = File("/data/data/com.example.experiment3/files/account_password.txt")
if (!file.exists()) {
val output = openFileOutput("account_password.txt", MODE_APPEND)
val writer = BufferedWriter(OutputStreamWriter(output))
writer.use {
it.write("admin\n")
it.write("123456\n")
it.write("student\n")
it.write("123456\n")
}
}
}
- 读写默认账号文件,键值对的方式保存。
//设置正确账号map表
val accountList = mutableMapOf<String, String>()
private fun setAccountList() {
val input = openFileInput("account_password.txt")
val reader = BufferedReader(InputStreamReader(input))
var line = 0
val account = ArrayList<String>()
val password = ArrayList<String>()
reader.use {
reader.forEachLine {
line += 1
if (line % 2 == 1)
account.add(it)
else if (line % 2 == 0)
password.add(it)
}
}
for (i in account.indices) {
accountList[account[i]] = password[i]
}
- 获得控件Text信息,用户名密码匹配。
匹配成功:
if (accountList[account] == password) {
Toast.makeText(this, "登陆成功", Toast.LENGTH_SHORT).show()
......
//进入管理员界面
//有且仅有一个管理员账号
if (account == "admin") {
......
val intent = Intent(this, AdminMenu::class.java)
startActivity(intent)
finish()
}
//其他全部进入学生界面
else {
......
val intent = Intent(this, StudentMenu::class.java)
startActivity(intent)
finish()
}
}
匹配失败:
else {
AlertDialog.Builder(this).apply {
setTitle("登陆失败")
setMessage("请重新检查用户名与密码。\n或者联系管理员。")
setCancelable(false)
setPositiveButton("OK") { _, _ -> }
show()
}
passwordEdit.text = null
}
其他:
1.在布局文件中的EditText中添加属性android:singleLine=“true”,防止用户输入回车导致形成多行文字输入。
2.EditText中添加属性android:inputType=“textPassword”,输入的文字会以···显示,防止密码泄露。
②记住密码功能
主要技术参考章节:7.3SharePreferences存储
思路:
- 进入登录页面时,检查prefs中“remember_password”是否为true,true则将保存的用户名密码直接显示在输入框内,否则不做显示。
- 登录账号时将用户名密码以及是否记住密码选项存入SharePreferences为下次登录做准备。
具体核心代码展示:
- 登陆时存入SharePreferences
val editor=prefs.edit()
if(rememberPass.isChecked){
editor.putBoolean("remember_password",true)
editor.putString("account",account)
editor.putString("password",password)
}else{
editor.clear()
}
- 登录时检查上次是否保记住密码
//记住密码功能
val prefs=getPreferences(Context.MODE_PRIVATE)
val isRemember=prefs.getBoolean("remember_password",false)
if(isRemember){
val account=prefs.getString("account","")
val password=prefs.getString("password","")
accountEdit.setText(account)
passwordEdit.setText(password)
rememberPass.isChecked=true
}
③设置个人信息
主要技术参考章节:7.3SharePreferences存储
思路:
- 从SharePreferences读取登录用户的信息
具体核心代码展示:
var user_name = "用户名:"
var user_identity = "身份:"
val prefs=getSharedPreferences("LoginUI.LoginActivity", MODE_PRIVATE)
user_name+=prefs.getString("account","null")
user_identity+=prefs.getString("identity","null")
userName.text = user_name
userIdentity.text = user_identity
④强制下线
主要技术参考章节:6全局大喇叭,广播机制
思路:
- 设计BaseActicvity作为Activity的基类,每创建一个新的活动就加入ActivityCollector。每当接收到强制下线的广播通知时,就调用ActivityCollector回收所有活动,回到登陆界面。
- ForceOfflineReceiver继承自BroadcastReceiver,当收到对应广播时,弹出提示,并返回界面。
- 强制下线按钮被点击时发出广播信息。
具体核心代码展示:
- ActivityCollector对象
object 1.ActivityCollector {
private val activities = ArrayList<Activity>()
fun addActivity(activity: Activity) {
activities.add(activity)
}
fun removeActivity(activity: Activity) {
activities.remove(activity)
}
fun finishAll() {
for (activity in activities) {
if (!activity.isFinishing) {
activity.finish()
}
}
activities.clear()
}
}
- BaseActivity基类与ForceOfflineReceiver接收器
open class BaseActivity : AppCompatActivity() {
lateinit var receiver: ForceOfflineReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityCollector.addActivity(this)
}
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter()
intentFilter.addAction("com.example.experiment3.FORCE_OFFLINE")
receiver = ForceOfflineReceiver()
registerReceiver(receiver, intentFilter)
registerReceiver(receiver, intentFilter)
}
override fun onPause() {
super.onPause()
unregisterReceiver(receiver)
}
override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivity(this)
}
inner class ForceOfflineReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
android.app.AlertDialog.Builder(context).apply {
setTitle("Warning")
setMessage("强制下线。请重新登录。")
setCancelable(false)
setPositiveButton("OK") { _, _ ->
ActivityCollector.finishAll()
val i = Intent(context, LoginActivity::class.java)
context.startActivity(i)
}
show()
}
}
- 强制下线按钮
ForceOffline.setOnClickListener() {
val intent = Intent("com.example.experiment3.FORCE_OFFLINE")
intent.setPackage(packageName)
sendBroadcast(intent)
}
其他:1. 必须将需要的Activity继承于BaseActivity,否则无法接收到广播信息。
⑤管理员菜单下拉框选择
主要技术参考章节:3 Activity跳转,其他
思路:
- 使用Spinner控件实现下拉框。
- 利用intent实现页面跳转,并将选择信息传递到下一个Activity。
具体核心代码展示:
- 初始化Spinner,并设置被选中时的文字样式。
val mItems = arrayOf("重要通知", "学术讲座", "深大新闻")
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, mItems)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
spinner.onItemSelectedListener = object : OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View, pos: Int, id: Long) {
val tv = view as TextView
tv.setTextColor(Color.BLUE)
tv.textSize = 20f
tv.gravity = Gravity.CENTER
}
}
- 点击事件
addInform.setOnClickListener() {
val intent = Intent(this, AdminAddInform::class.java)
intent.putExtra("column", spinner.selectedItem.toString())
startActivity(intent)
}
⑥ 管理员添加通知
主要技术参考章节:7.2文件存储
思路:
- 通过intent获取到用户选择选项,一开始将部分页面的布局属性设置成android:visibility=“invisible”,根据选择将部分页面布局进行展示。
- 将用户输入的内容追加读写入文件,当读到空串时返回报错信息。
具体核心代码展示:
- 页面布局(下拉框省略,大致同上)
//设置栏目
var column_title = "修改栏目:"
column_title += intent.getStringExtra("column")
columnTitle.text = column_title
//编辑布局
when (intent.getStringExtra("column")) {
"重要通知" -> {
informType.visibility = View.VISIBLE
informTitle.visibility = View.VISIBLE
blank3.visibility = View.VISIBLE
......
}
"学术讲座" -> {
dateTime.visibility = View.VISIBLE
place.visibility = View.VISIBLE
blank.visibility = View.VISIBLE
blank2.visibility = View.VISIBLE
informTitle.visibility = View.VISIBLE
}
"深大新闻" -> {
informTitle.visibility = View.VISIBLE
}
}
- 文档修改,以学术讲座为例
"重要通知" -> {
val output = openFileOutput("important_information.txt", MODE_APPEND)
val writer = BufferedWriter(OutputStreamWriter(output))
val type = informType.selectedItem.toString()
val content = informTitle.text.toString()
//如果未填写,做出反馈
if (content == "") {
Toast.makeText(this, "请正确输入内容", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
writer.use {
it.write(type)
it.newLine()
it.write(content)
t.newLine()
}
val intent = Intent(this, AdminMenu::class.java)
startActivity(intent)
finish()
}
⑦平板手机自适应
主要技术参考章节:5 探究fragment
思路:
- 将左右界面设计为fragment,并且将右fragment的可见性设置为invisible。
- 判断当前设备的屏幕大小,如果为平板则将右fragment可见性设置为visible,形成双栏展示的效果。
具体核心代码展示:
- 判断手机或平板,是否要双栏展示。
private var isTwoPane = false
isTwoPane = activity?.findViewById<View>(R.id.StudentRightLayout) != null
- 刷新左右布局
class RightMainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_right_main)
val fragment = RightMainFrag as RightFragment
supportActionBar?.hide()
fragment.refresh()
}
}
fun refresh() {
contentLayout.visibility = View.VISIBLE
}
- 平板/手机xml布局
手机:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
tools:context=".Student.StudentMenu">
<fragment
android:id="@+id/StudentLeftFrag"
android:name="com.example.experiment3.Student.LeftFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
平板:左右1.65:3平分宽度
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/StudentLeftFrag"
android:name="com.example.experiment3.Student.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.65" />
<FrameLayout
android:id="@+id/StudentRightLayout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3">
<fragment
android:id="@+id/StudentRightFrag"
android:name="com.example.experiment3.Student.RightFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</LinearLayout>
⑧新闻列表刷新
主要技术参考章节:7.2 文件存储,4.6 RecyclerView,3 活动
思路:
- 按行读取对应文件内容,插入RecyclerView中实现新闻刷新。
- 点击新闻头时,触发点击事件刷新按钮颜色,以及不同新闻栏的切换。
- 点击其他–内部网时,根据平板或手机选择是刷新界面或者活动跳转。
展示的效果。
具体核心代码展示:
1.颜色刷新,新闻栏切换
important_inform_button.setOnClickListener() {
important_inform_button.setBackgroundColor(Color.BLUE)
important_inform_button.setTextColor(Color.WHITE)
academic_lecture_button.setBackgroundColor(Color.TRANSPARENT)
academic_lecture_button.setTextColor(Color.RED)
szu_news_button.setBackgroundColor(Color.TRANSPARENT)
szu_news_button.setTextColor(Color.RED)
importantInformRecyclerView.visibility = View.VISIBLE
academicLectureRecyclerView.visibility = View.GONE
szuNewsRecyclerView.visibility = View.GONE
}
- 读取文件(以学术讲座为例)(与前面介绍的管理员添加通知形成呼应,如添加会有显示)
private fun getInform2(): ArrayList<academic_lecture> {
val informList = ArrayList<academic_lecture>()
val input = activity?.openFileInput("academic_lecture.txt")
val reader = BufferedReader(InputStreamReader(input))
var line = 0
val date = ArrayList<String>()
val title = ArrayList<String>()
val place = ArrayList<String>()
reader.use {
reader.forEachLine {
line += 1
if (line % 3 == 1)
date.add(it)
else if (line % 3 == 0) {
place.add(it)
} else if (line % 3 == 2)
title.add(it)
}
}
for (i in date.indices) {
informList.add(academic_lecture(date[i], title[i], place[i]))
}
return informList
}
- recyclerView设置(以学术讲座为例)
//设置学术讲座
val layoutManager3 = LinearLayoutManager(activity)
szuNewsRecyclerView.layoutManager = layoutManager3
val adapter3 = StudentMenu.SzuNewsAdapter(getInform3())
szuNewsRecyclerView.adapter = adapter3
//学术讲座RecyclerViewAdapter
class AcademicLectureAdapter(private val informList: List<academic_lecture>) :
RecyclerView.Adapter<AcademicLectureAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val AcademicLectureDate: TextView = view.findViewById(R.id.lecturedate)
val AcademicLectureTitle: TextView = view.findViewById(R.id.lecturetitle)
val AcademicLecturePlace: TextView = view.findViewById(R.id.lectureplace)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.academic_lecture_item, parent, false)
return ViewHolder(view)
}
override fun getItemCount() = informList.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
var inform = informList[position]
holder.AcademicLectureTitle.text = inform.title
holder.AcademicLectureDate.text = inform.date
holder.AcademicLecturePlace.text = inform.place
}
}
注:visibility属性中Gone与Invisible的区别是Gone不保留原控件所占位置,而invisible保存原控件所占位置。
- 根据平板或手机选择是刷新界面或者活动跳转
szu_website_button.setOnClickListener() {
//手机版
if (!isTwoPane) {
val intent = Intent(this, RightMainActivity::class.java)
startActivity(intent)
}
//平板版
else {
contentLayout.visibility=View.VISIBLE
right1.textSize = 15F
right2.textSize = 15F
right3.textSize = 15F
right4.textSize = 15F
}
}
⑨网页活动跳转
主要技术参考章节:3 活动
思路:
- 点击办事大厅实现网页跳转(其他按钮功能类似,没有做实现)
具体核心代码展示:
task1_1.setOnClickListener(){
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("http://ehall.szu.edu.cn/new/index.html")
startActivity(intent)
}
三、实验总结
本次实验在完成基本要求的基础上,加入了自己的一些理解与创新。
实验过程中遇到了两个比较大的问题是
-
添加默认账号列表时会有多次重复添加的问题
一开始时的思路时在打开应用时,每次都向指定文件中加入默认账号信息。由于提取信息是通过map键值对的方式,所以并没有影响,也没有做修改。但是当用户使用次数逐渐增多时,文件内容越积越多,显然不合理。
所以经过查询,发现可以通过查询文件是否已经存在,来判断是否要加入新信息。即通过if (!file.exists())来判断,从而提高效率。 -
利用Intent传输活动间信息间信息丢失
一开始时通过intent.putExtraString()的方式向下一个活动传递用户名,身份信息。但是随着开发的进行,发现当活动通过其他方式被唤醒时会出来没有intent传递信息的情况,导致获取到空串。
所以我选择将用户信息通过Share Preference的方式进行存储,这样就可以随时随地获取账号信息了。
总体而言,本次实验结合了各种技术,比较好的完成了深大内部网的复现任务。