源码: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/%E6%9C%9F%E6%9C%AB%E5%A4%A7%E4%BD%9C%E4%B8%9A
具体要求
模拟图1所示垃圾分类APP,介绍垃圾分类与回收相关的一些知识点并能提供相应服务:
部分参考
其它要求
评分标准
注:测试环境为华为nova5 pro
具体项目构建过程可以参考github
URL:https://github.com/Alex-Shen1121/GarbageClassificationAndroidAPP
此部分将大致展示APP构建过程及效果图,具体代码实现及原理将在下一部分具体展示。
第一步做的任务是设计APP登录界面,首先是一个欢迎界面的设计。通过查询网上资料,完成动态动画设计。
第二步是登录与注册页面的设计,主要利用的技术来自书本数据持久化这一章节,完成了登录,注册,记住密码等功能。
第三步是开始设计应用的主要界面。可以通过点击APP下方的项目选项按钮,跳转到不同的访问界面
第四步是分类百科界面的设计。过程中利用了大量时间去学习如何使用Tablayout与ViewPager的结合,其中花费了许多时间在调试上,最终完成了与源程序大致相同的实现效果,实现了左右滑动切换fragment的效果。
最终整个APP从设计到完成大约花费一周的时间,实现了绝大部分目标APP的功能模拟,并加入了部分自己的理解,比较好的复现了APP。
以下部分按照此顺序介绍各部分功能:
主要技术:ViewCompat,Activity,UI设计,Intent
首先将一张图片存放在ImageView之中撑满整个画面,然后调用ViewCompat.animate方法并设置监听,对图像做伸缩变换以及动态时长控制。最后在动画结束的时候退出并进入登陆界面。
1.//设置图片动画
2.ViewCompat.animate(imageView).apply {
3. //缩放,变成1.0倍
4. scaleX(1.0f)
5. scaleY(1.0f)
6. //动画时常1秒
7. duration = 1000
8. //动画监听
9. setListener(object : ViewPropertyAnimatorListener {
10. override fun onAnimationEnd(view: View?) { //动画结束
11. //进入主界面,并结束掉该页面
12. startActivity(Intent(this@WelcomeActivity, LoginActivity::class.java))
13. finish()
14. })
15. }
主要技术:数据持久化,Activity,UI设计,AlertDialog,Intent
通过文件读写的方式,添加默认用户名及密码,方便用户第一次体验APP
1.val output = openFileOutput("account_password.txt", MODE_APPEND)
2. val writer = BufferedWriter(OutputStreamWriter(output))
3. writer.use {
4. it.write("admin\n")
5. it.write("123456\n")
6. }
登陆的时候,通过文件读写的方式获取APP的账户列表,后期可以通过连接云端服务器。
1.reader.forEachLine {
2. line += 1
3. if (line % 2 == 1)
4. account.add(it)
5. else if (line % 2 == 0)
6. password.add(it)
7.}
将正确的用户名密码以键值对的方式保存。
accountList[account[i]] = password[i]
与用户输入的用户名密码进行匹配,如果完全匹配则进入相对应界面,否则弹出错误提示,并删去密码,重新输入(更加符合使用习惯)。
匹配成功:效果是进入主界面
1.val intent = Intent(this, MainPage::class.java)
2.startActivity(intent)
1. AlertDialog.Builder(this).apply {
2. setTitle("登陆失败")
3. setMessage("请重新检查用户名与密码。\n或者联系管理员。")
4. setCancelable(false)
5. setPositiveButton("OK") { _, _ -> }
6. show()
7.}
8.passwordEdit.text = null
记住密码功能。如果勾选了,用户在下一次登录(无论是强制退出或者重启APP)都可以免去重新输入账号密码的过程,利用的技术是SharePreferences数据持久化。
进入登录页面时,检查prefs中“remember_password”是否为true,true则将保存的用户名密码直接显示在输入框内,否则不做显示。
1.val prefs = getPreferences(Context.MODE_PRIVATE)
2. val isRemember = prefs.getBoolean("remember_password", false)
3. if (isRemember) {
4. val account = prefs.getString("account", "")
5. val password = prefs.getString("password", "")
6.
7. accountEdit.setText(account)
8. passwordEdit.setText(password)
9. rememberPass.isChecked = true
10. }
登录账号时将用户名密码以及是否记住密码选项存入SharePreferences为下次登录做准备。
1.if (rememberPass.isChecked) {
2. editor.putBoolean("remember_password", true)
3. editor.putString("account", account)
4. editor.putString("password", password)
5. }
6.editor.apply()
主要技术:数据持久化,UI设计,Intent
注册过程会进行三层检查只有都通过了才会注册成功。
第一层:检查是否为空串,否则返回“请正确输入信息”
1. if (name == "" || password1 == "" || password2 == "") {
2. passwordEdit.text = null
3. passwordCheckEdit.text = null
4. Toast.makeText(this, "请正确输入信息", Toast.LENGTH_SHORT).show()
5.}
第二层:检查用户名是否存在,调用contains函数,否则返回“该用户名已存在,请重新输入”
1.if (accountList.contains(name)) {
2. Toast.makeText(this, "该用户名已存在,请重新输入", Toast.LENGTH_SHORT).show()
3. passwordEdit.text = null
4. passwordCheckEdit.text = null
5. accountEdit.text = null
6.}
第三层:检查密码与确认密码是否完全相同,否则返回“两次密码不一致,请重新输入”
1.if(password1!=password2){
2. Toast.makeText(this, "两次密码不一致,请重新输入", Toast.LENGTH_SHORT).show()
3. passwordEdit.text = null
4. passwordCheckEdit.text = null
5.}
当用户输入的账号及密码通过了这三层检查,就可以完成注册,即将用户输入的信息通过文件读取的方式,写入txt文本中,实现注册功能。例如在此处注册了test01-123456的账户。
1.val output = openFileOutput("account_password.txt", MODE_APPEND)
2. val writer = BufferedWriter(OutputStreamWriter(output))
3. writer.use {
4. it.write(name+'\n')
5. it.write(password1+'\n')
6. }
Toast.makeText(this, "注册成功", Toast.LENGTH_SHORT).show()
思路:主界面其实只包含了一个界面,其中在屏幕正中放置了三个LinearLayout并将属性设置为了android:visibility=“gone”,当需要展示时就点击下方按钮,就将对应的布局设置为可见,并刷新下方按钮的属性,实现不同界面的切换功能。
按钮属性切换:效果是将正在展示的界面变为彩色并且着重文字效果。
1.titleText.text = "垃圾分类"
2.button1.setImageResource(R.drawable.pic12)
3.text1.textSize = 20F
4.text1.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
界面切换:效果是展示不同的界面
1.column1.visibility = View.VISIBLE
主要技术:TabLayout+ViewPager,UI设计,Fragment碎片
思路:
1.viewpager.adapter = object : FragmentPagerAdapter(supportFragmentManager) {
2. override fun getItem(position: Int): Fragment {
3. return Lfragments[position]
4. }
5. override fun getCount(): Int {
6. return Lfragments.size
7. }
8. override fun getPageTitle(position: Int): CharSequence? {
9. return Ltitles[position]
10. }
11.}
1.mTabText.text = Ltitles[i]
2.mTabIcon.setImageResource(Limg[i])
3.//更改选中项样式
4.if (i === ItemWhat) {
5. mTabIcon.setImageResource(Limg[i])
6. mTabText.setTextColor(ContextCompat.getColor(this, R.color.purple_200))
7.}
8.//设置样式
9.tabs2.getTabAt(i)?.customView = view
tabs2.getTabAt(ItemWhat)?.select()
1.tabs2.setupWithViewPager(viewpager)
最终效果就如下图
5. 为每一个tab编写一个fragment并重写其中的onActivityCreated以及onCreateView方法。以可回收垃圾为例。
1.override fun onActivityCreated(savedInstanceState: Bundle?) {
2. super.onActivityCreated(savedInstanceState)
3. picture.setImageResource(R.drawable.frag1)
4. title1.text="可回收物是指"
5. task1.text="废纸张、废塑料、废玻璃制品、废金属、废织物等适宜回收、可循环利用的生活废弃品。"
6. title2.text="可回收物投放要求"
7. task2.text="1. 轻投轻放\n2. 清洁干燥,避免污染\n3. 废纸尽量平整\n"
8. }
9.
10. override fun onCreateView(
11. inflater: LayoutInflater,
12. container: ViewGroup?,
13. savedInstanceState: Bundle?
14. ): View? {
15. return inflater.inflate(R.layout.fragment,container,false)
16. }
1.override fun onTabSelected(tab: TabLayout.Tab) {
2. //选中时进入,改变样式
3. ItemSelect(tab)
4. //onTabselected方法里面调用了viewPager的setCurrentItem 所以要想自定义OnTabSelectedListener,也加上mViewPager.setCurrentItem(tab.getPosition())就可以了
5. viewpager.currentItem = tab.position
6.}
7.
8.override fun onTabUnselected(tab: TabLayout.Tab) {
9. //未选中进入,改变样式
10. ItemNoSelect(tab)
11.}
被选中时,会调用ItemSelect函数;未被选中时,会调用ItemNoSelect函数,效果是刷新tab样式。ItemNoSelect类似,不做展示。
1.//某个项选中,改变其样式
2.vate fun ItemSelect(tab: TabLayout.Tab) {
3. val customView = tab.customView
4. val tabText = customView!!.findViewById<View>(R.id.item_text) as TextView
5. val tabIcon: ImageView = customView.findViewById<View>(R.id.item_img) as ImageView
6. tabText.setTextColor(ContextCompat.getColor(this, R.color.purple_200))
7. val stitle = tabText.text.toString()
8. for (i in Ltitles.indices) {
9. if (Ltitles[i] == stitle) {
10. tabIcon.setImageResource(Limg[i])
11. }
12. }
13. }
最终效果就是可以通过屏幕的左右滑动,实现fragment之间的平滑的切换。
主要技术:UI设计,Activity,Intent
在页面中方式orientation属性为vertical的LinearLayout,在其中一次放置了6个选项,并且为每一个Layout设计Activity跳转事件。(以切换地区为例)
1.changePlace.setOnClickListener(){
2. val intent= Intent(this, PlaceActivity::class.java)
3. startActivity(intent)
4.}
主要技术:数据持久化,UI设计,RecyclerView,Intent
1.inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
2. val placeName: TextView = view.findViewById(R.id.placeName)
3.}
onCreateViewHolder方法:
1.override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
2. val view = LayoutInflater.from(parent.context)
3. .inflate(R.layout.place_item, parent, false)
4. val viewHolder = ViewHolder(view)
5. return viewHolder
6.}
onBindViewHolder方法
1.override fun onBindViewHolder(holder: ViewHolder, position: Int) {
2. val placename = place_List[position]
3. holder.placeName.text = placename
4.}
getItemCount方法
1.override fun getItemCount() = place_List.size
1.val layoutManager = LinearLayoutManager(this)
2.recyclerView.layoutManager = layoutManager
3.val adapter1 = place_Adapter(placeList)
4.recyclerView.adapter = adapter1
1.val position: Int = viewHolder.layoutPosition
2.val placeName = place_List[position]
3.val prefs = parent.context.getSharedPreferences("current_place", Context.MODE_PRIVATE)
4.val editor = prefs.edit()
5.editor.putString("place", placeName)
6.editor.apply()
7.Toast.makeText(parent.context,"已成功修改为$placeName",Toast.LENGTH_SHORT).show()
1.val prefs = getSharedPreferences("current_place", Context.MODE_PRIVATE)
2.val current_place = prefs.getString("place", null)
3.currentPlace.text = current_place
4.finish()
主要技术:Activity,Intent
效果是从手机APP跳转到手机应用商城之中为APP打分。经过上网查询,获得了以下各个常见手机商城的对应Intent跳转目录。
地址 | 应用 |
---|---|
com.android.vending | Google Play |
com.tencent.android.qqdownloader | 应用宝 |
com.qihoo.appstore | 360手机助手 |
com.baidu.appsearch | 百度手机助手 |
com.xiaomi.market | 小米应用商店 |
com.wandoujia.phoenix2 | 豌豆荚 |
com.huawei.appmarket | 华为应用市场 |
com.taobao.appcenter | 淘宝手机助手 |
com.hiapk.marketpho | 安卓市场 |
cn.goapk.market | 安智市场 |
从APP跳转到其他商城APP,可以通过调用launchAppDetail(),并且传入两个参数,第一个是appPkg,第二个是marketPkg。因为这个APP并没有上架商城,所以查询不到。
1.launchAppDetail("com.xxxxxx", "com.huawei.appmarket")
主要技术:Activity,Intent
跳转到编写了《关于我们》页面的活动
1.val intent= Intent(this, AboutUsActivity::class.java)
2.startActivity(intent)
主要技术:Activity,Intent,ScrollView
ScrollView控件之中的内容可以无限长,并且通过上下滑动的方式扩展页面
主要技术:Activity,Intent
APP权限页面跳转目录为ACTION_APPLICATION_DETAILS_SETTINGS
1.val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
2.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
3.val uri = Uri.fromParts("package", packageName, null)
4.intent.data = uri
5.startActivity(intent)
主要技术:广播机制,继承类
ActivityCollecor类:
添加活动:
1.activities.add(activity)
移除活动:
2.activities.remove(activity)
结束所有活动:
1.for (activity in activities) {
2. if (!activity.isFinishing) {
3. activity.finish()
4. }
5.}
6.activities.clear()
BaseActivity类:
作为Activity类的基类:
1.open class BaseActivity : AppCompatActivity()
设置监听事件:
1.override fun onResume() {
2. super.onResume()
3. val intentFilter = IntentFilter()
4. intentFilter.addAction("com.example.experiment3.FORCE_OFFLINE")
5. receiver = ForceOfflineReceiver()
6. registerReceiver(receiver, intentFilter)
7.}
设置监听到广播时的反馈事件(弹出下线的窗口,关闭所有活动并跳转回登陆界面):
1.android.app.AlertDialog.Builder(context).apply {
2. setTitle("Warning")
3. setMessage("请重新登录。")
4. setCancelable(false)
5. setPositiveButton("OK") { _, _ ->
6. ActivityCollector.finishAll()
7. val i = Intent(context, LoginActivity::class.java)
8. context.startActivity(i)
9. }
10. show()
11.}
思路:首页之中包含了两个部分,第一个部分是垃圾分类的查询功能,第二个部分是一个垃圾分类的科普视频。
主要技术:VideoView
1.val uri = Uri.parse("android.resource://$packageName/${R.raw.video}")
2.videoView.setVideoURI(uri)
3.videoView.requestFocus()
1.videoView.setMediaController(MediaController(this))
主要技术:数据持久化,SQLite,UI设计,RelativeLayout
1.private val createTrashTable="create table TrashTable ("+
2. " name text primary key,"+
3. "type text)"
4.
5.override fun onCreate(db: SQLiteDatabase?) {
6. db?.execSQL(createTrashTable)
7.}
1.val trashList1= arrayListOf<String>(...)
2.for(i in trashList1){
3. val value=ContentValues().apply {
4. put("name",i)
5. put("type","干垃圾")
6. }
7. db.insert("TrashTable",null,value)
8.}
1.select * from TrashTable where name like '%$targetItem%'
1.if(cursor.moveToFirst()){
2. do{
3. val name=cursor.getString(cursor.getColumnIndex("name"))
4. val type=cursor.getString(cursor.getColumnIndex("type"))
5. searchList.add(search_result(name,type))
6. }while (cursor.moveToNext())
7.}
其中,每一栏的图片根据传进来的type数据,判断应该采用哪一张图片
1.when(Type){
2. "可回收物"->holder.typeimage.setImageResource(R.drawable.trash1)
3. "湿垃圾"->holder.typeimage.setImageResource(R.drawable.trash2)
4. "干垃圾"->holder.typeimage.setImageResource(R.drawable.trash3)
5. "有害垃圾"->holder.typeimage.setImageResource(R.drawable.trash4)
6.}
6. 设置recyclerView的点击事件
根据传进来的type数据,判断AlertDialog应该发出什么提示信息。
本次实验在完成基本要求的基础上,加入了自己的一些理解与创新。
实验过程中遇到了两个比较大的问题是
添加默认账号列表时会有多次重复添加的问题
一开始时的思路时在打开应用时,每次都向指定文件中加入默认账号信息。由于提取信息是通过map键值对的方式,所以并没有影响,也没有做修改。但是当用户使用次数逐渐增多时,文件内容越积越多,显然不合理。
所以经过查询,发现可以通过查询文件是否已经存在,来判断是否要加入新信息。即通过if (!file.exists())来判断,从而提高效率。
利用Intent传输活动间信息间信息丢失
一开始时通过intent.putExtraString()的方式向下一个活动传递用户名,身份信息。但是随着开发的进行,发现当活动通过其他方式被唤醒时会出来没有intent传递信息的情况,导致获取到空串。
所以我选择将用户信息通过Share Preference的方式进行存储,这样就可以随时随地获取账号信息了。
在布局文件中的EditText中添加属性android:singleLine=“true”,防止用户输入回车导致形成多行文字输入。
EditText中添加属性android:inputType=“textPassword”,输入的文字会以···显示,防止密码泄露。
切换页面视频播放
如果页面在播放就切换了页面,是不符合逻辑的。所以在每一次切换前,都加入一层逻辑判断
1.if (videoView.isPlaying) {
2. videoView.pause()
3.}
这样就可以保证切换页面时,不会在传出视频声音。
总体而言,本次实验结合了各种技术,比较好的完成了垃圾分类APP的复现任务。