【移动设备交互及应用】我的校园安卓APP设计

Alex_Shen
2021-02-21 / 2 评论 / 0 点赞 / 382 阅读 / 13,479 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-04-06,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

源码:https://github.com/Alex-Shen1121/SZU_Learning_Resource/tree/main/计算机与软件学院/移动设备交互应用/实验/实验3-我的校园

一、实验目的与内容:

目的:掌握安卓中活动的编写、自定义用户界面的开发、碎片开发、广播机制以及数据持久化技术等;并能通过对课堂知识进行扩展来完善该界面,并使界面尽量美观。
内容要求:

  1. 请尽量模拟如下深大校园主页的功能,参考:
    https://www1.szu.edu.cn/
    在这里插入图片描述

  2. 具体要求:

    1. 该实现的界面在某些地方应体现出如下功能:
      a. 界面能对平板与手机平台进行自适应(参考第4章碎片);
      b. 能对用户身份有强制下线的功能,比如网络中断,登录界面强行退出并显示提示错误的界面;
      c. 界面某些地方体现数据持久化的技术,如文件数据的读取、存储的多种实现方式,并简单阐述几种实现方式具体的适用场景;
      d. 界面要比较工整,没必要实现参考界面上的所有子项,能保证自己的界面实现能有扩展到参考界面的能力即可。
    2. 功能并不局限于上面的要求,可以根据自己的理解设计一些新的功能,并在报告文档中进行详细的阐述,作为报告的亮点;
      3)APP的布局尽快模仿参考界面,如果有较大的困难,可以只实现出右半边部分的界面,并尽量按上面要求进行完善;
    3. 对于某一种功能,可以在不同的子项处采用多种实现方式,并比较这些实现方式的不同及优劣势。
  3. 参考:尽量多的应用参考书《第一行代码 Android》第二版第2章活动、第3章UI开发第4章碎片、第5章广播机制与第6章数据持久化技术的各个知识点。

注意:

  1. 实验报告中需要有功能的描述、实验结果的截屏图像及详细说明;
  2. 也欢迎采用其它章节的知识点完成本次实验报告,如果实现的功能言之合理,会考虑酌情加分。

二、实验过程和代码与结果

“我的校园”APP的构建过程及结果

注:测试环境为华为nova5 pro 6.39寸手机与虚拟机Nexus 9 8.86寸平板。
界面展示:
主页:
(平板)在这里插入图片描述(手机) 在这里插入图片描述
学生界面:
在这里插入图片描述 在这里插入图片描述 在这里插入图片描述
在这里插入图片描述 在这里插入图片描述
管理员界面:
在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述
构建过程:
具体项目构建过程可以参考github
URL:https://github.com/Alex-Shen1121/SZU_Website_Android

  1. 编写Activity基类与ActivityCollector为实现强制下线功能打下基础
  2. 设计登陆界面布局,编写账号登录逻辑
  3. 设计编写管理员主界面,完成用户信息展示
  4. 实现强制下线功能
  5. 设计管理员修改界面,完成文件的修改逻辑
  6. 设计学生界面,并完成左右Fragment的平板手机自适应功能
  7. 编写学生界面各个布局的内容填充
  8. 实现网页的跳转
  9. 修复部分bug

具体各个部分的关键代码会在下一个部分进行展示。

请详细说明“我的校园”APP的功能、出现的关键问题及解决方案

“我的校园”APP的功能亮点介绍及代码分析:
(以下截图将以手机页面进行展示,平板上有基本一致的效果)
本次APP提供两个默认账号用于登录。
学生账户用户名:student 学生账户密码:123456
管理员账户用户名:admin 管理员账户密码:123456

①账号登陆匹配

主要技术参考章节:7.2文件存储,3 Activity
思路:

  1. 通过文件读写的方式获取APP的默认账户,后期可以通过连接云端服务器。
  2. 将正确的用户名密码以键值对的方式保存。
  3. 与用户输入的用户名密码进行匹配,如果完全匹配则进入相对应界面,否则弹出错误提示,并删去密码,重新输入(更加符合使用习惯)。

具体核心代码展示:

  1. 添加默认账号文件
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")
            }
        }
    }

在这里插入图片描述

  1. 读写默认账号文件,键值对的方式保存。
//设置正确账号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]
        }
  1. 获得控件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存储
思路:

  1. 进入登录页面时,检查prefs中“remember_password”是否为true,true则将保存的用户名密码直接显示在输入框内,否则不做显示。
  2. 登录账号时将用户名密码以及是否记住密码选项存入SharePreferences为下次登录做准备。

具体核心代码展示:

  1. 登陆时存入SharePreferences
val editor=prefs.edit()
if(rememberPass.isChecked){
  editor.putBoolean("remember_password",true)
  editor.putString("account",account)
  editor.putString("password",password)
}else{
  editor.clear()
}

在这里插入图片描述

  1. 登录时检查上次是否保记住密码
//记住密码功能
        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存储
思路:

  1. 从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全局大喇叭,广播机制
思路:

  1. 设计BaseActicvity作为Activity的基类,每创建一个新的活动就加入ActivityCollector。每当接收到强制下线的广播通知时,就调用ActivityCollector回收所有活动,回到登陆界面。
  2. ForceOfflineReceiver继承自BroadcastReceiver,当收到对应广播时,弹出提示,并返回界面。
  3. 强制下线按钮被点击时发出广播信息。

具体核心代码展示:

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

在这里插入图片描述

  1. 强制下线按钮
ForceOffline.setOnClickListener() {
val intent = Intent("com.example.experiment3.FORCE_OFFLINE")
intent.setPackage(packageName)
sendBroadcast(intent)
}

其他:1. 必须将需要的Activity继承于BaseActivity,否则无法接收到广播信息。

⑤管理员菜单下拉框选择

主要技术参考章节:3 Activity跳转,其他
思路:

  1. 使用Spinner控件实现下拉框。
  2. 利用intent实现页面跳转,并将选择信息传递到下一个Activity。

具体核心代码展示:

  1. 初始化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
}
}

在这里插入图片描述 在这里插入图片描述

  1. 点击事件
addInform.setOnClickListener() {
    val intent = Intent(this, AdminAddInform::class.java)
    intent.putExtra("column", spinner.selectedItem.toString())
    startActivity(intent)
}

⑥ 管理员添加通知

主要技术参考章节:7.2文件存储
思路:

  1. 通过intent获取到用户选择选项,一开始将部分页面的布局属性设置成android:visibility=“invisible”,根据选择将部分页面布局进行展示。
  2. 将用户输入的内容追加读写入文件,当读到空串时返回报错信息。

具体核心代码展示:

  1. 页面布局(下拉框省略,大致同上)
//设置栏目
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
    }
}

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

  1. 文档修改,以学术讲座为例
"重要通知" -> {
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
思路:

  1. 将左右界面设计为fragment,并且将右fragment的可见性设置为invisible。
  2. 判断当前设备的屏幕大小,如果为平板则将右fragment可见性设置为visible,形成双栏展示的效果。

具体核心代码展示:

  1. 判断手机或平板,是否要双栏展示。
private var isTwoPane = false
isTwoPane = activity?.findViewById<View>(R.id.StudentRightLayout) != null
  1. 刷新左右布局
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
}
  1. 平板/手机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 活动
思路:

  1. 按行读取对应文件内容,插入RecyclerView中实现新闻刷新。
  2. 点击新闻头时,触发点击事件刷新按钮颜色,以及不同新闻栏的切换。
  3. 点击其他–内部网时,根据平板或手机选择是刷新界面或者活动跳转。
    展示的效果。

具体核心代码展示:
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
        }
  1. 读取文件(以学术讲座为例)(与前面介绍的管理员添加通知形成呼应,如添加会有显示)
    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
    }
  1. 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保存原控件所占位置。

  1. 根据平板或手机选择是刷新界面或者活动跳转
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 活动
思路:

  1. 点击办事大厅实现网页跳转(其他按钮功能类似,没有做实现)

具体核心代码展示:

task1_1.setOnClickListener(){
    val intent = Intent(Intent.ACTION_VIEW)
    intent.data = Uri.parse("http://ehall.szu.edu.cn/new/index.html")
    startActivity(intent)
}

在这里插入图片描述

三、实验总结

本次实验在完成基本要求的基础上,加入了自己的一些理解与创新。
实验过程中遇到了两个比较大的问题是

  1. 添加默认账号列表时会有多次重复添加的问题
    一开始时的思路时在打开应用时,每次都向指定文件中加入默认账号信息。由于提取信息是通过map键值对的方式,所以并没有影响,也没有做修改。但是当用户使用次数逐渐增多时,文件内容越积越多,显然不合理。
    所以经过查询,发现可以通过查询文件是否已经存在,来判断是否要加入新信息。即通过if (!file.exists())来判断,从而提高效率。

  2. 利用Intent传输活动间信息间信息丢失
    一开始时通过intent.putExtraString()的方式向下一个活动传递用户名,身份信息。但是随着开发的进行,发现当活动通过其他方式被唤醒时会出来没有intent传递信息的情况,导致获取到空串。
    所以我选择将用户信息通过Share Preference的方式进行存储,这样就可以随时随地获取账号信息了。

总体而言,本次实验结合了各种技术,比较好的完成了深大内部网的复现任务。

0

评论区