我的垃圾分类APP

  • 一、期末大作业的目的与要求:
  • 1. 垃圾分类界面
  • 2. 具体要求
  • 3. 部分参考
  • 4. 其它要求
  • 5. 评分标准
  • 二、实验过程和代码与结果
  • 1. 实验亮点:
  • 2. “我的垃圾分类APP”的构建过程及结果
  • (1)欢迎页面的实现:
  • (2)垃圾分类的实现:
  • ①获取API数据:
  • ②进行数据展示:
  • ③查询详细信息:
  • (3)语音识别:
  • ①首先申请录音权限:
  • ②判断服务器是否就绪,若就绪则可以获取用户语音输入:
  • ③当用户语音输入时,要提示出开始录音与录音结束
  • ④当用户输入完成时,及判断是否为有效输入
  • (4)图片识别:
  • ①与语音识别相同,首先要申请权限,如果获得权限则接口就绪,否则弹出权限不足的提示。
  • ②获取上传的图片后,调用对应的API接口,然后获取返回数据,并进行垃圾种类的判断。
  • (5)网络监测:
  • (6)版本更新
  • ①首先定义获取更新类:
  • ②更新检测(判断是否有更新版本):
  • ③弹出更新对话框:
  • ④开始下载更新文件:
  • ⑤此外,如果发生错误或警告,需要提示用户取消
  • (7)调用商店进行评分
  • (8)退出提示:
  • (9)打包成APK:
  • 3. 请详细说明“我的垃圾分类APP”的功能、出现的关键问题及解决方案
  • (1)主要功能:
  • ①登录预览页:
  • ②主页:
  • ③语音输入
  • ④图片识别:
  • ⑤版本更新:
  • ⑥APP评价:
  • ⑦主页上的几个按键:
  • (2)遇到的问题及解决办法:
  • ①**Q:ViewGroup为什么不会调用onDraw
  • ②加载条死循环
  • ③使用Logcat进行debug
  • ④ScrollView和ListView两个View都有滚动的效果,在嵌套使用时起冲突的问题
  • ⑤Theme.AppCompat.Light报错
  • ⑥偶发AndroidStudio进入界面卡死,无法进入
  • 三、实验总结


一、期末大作业的目的与要求:

1. 垃圾分类界面

请尽量模拟如下垃圾分类APP的功能,即参考如下的界面展示形式及功能模块:

移动端 用户与组件的交互数据分析_安卓


移动端 用户与组件的交互数据分析_安卓_02


移动端 用户与组件的交互数据分析_移动端 用户与组件的交互数据分析_03


移动端 用户与组件的交互数据分析_数据_04

2. 具体要求

模拟图1所示垃圾分类APP,介绍垃圾分类与回收相关的一些知识点并能提供相应服务:
1) 建议包含的一些功能:活动之间的转换与数据传递;能适应不同的展示界面;有登录功能,强制下线功能;数据有多样化的持久化功能;能跨程序提供与共享数据;有展示一些多媒体的功能;

2) 较好的实现了书本上介绍的一些较成熟的功能,并能较好的把这些功能融合在一个完整且无大bug的APP里;

3) 能在此基础上构建自己的报告亮点,如实现了书本不一样的功能模块,或者为某个知识点找到一些新的应用场景,或者能解决同学们普遍存在的一些问题等;

4) 模拟的APP不局限于所参照APP的功能,即尽量模拟这些功能,不要求将每个功能都实现,如果某个功能不能体现已学知识点,可以不用考虑,当然如果能想办法实现出来,可以作为报告亮点;即不必与这些功能完全一样,可在这些功能基础上进行变通,达到类似的效果就可以;可以设计一些该APP没有的功能,并能清楚说明这些功能的实现方式、潜在的用途等;同时布局的设计也不必与参考APP完全一样,可根据自己需要适当调整;

5) 总体目标是灵活利用所学的知识点,做到每个功能各种实现方式的丰富化(如数据的持久化的三种实现方式都能在APP中有所体现),并且能体现不同实现方式的优劣,如果能在APP上体现会更好;

3. 部分参考

1)功能实现参考:图1第2列图尽量参考第6章数据持久化技术的各个知识点;第3列尽量参考布局及活动之间的跳转,碎片的实现,多媒体展示功能;第4列可以利用数据持久化技术;

2)潜在的扩展功能:图1第4列尽量参考Android基于位置的服务;添加一个小功能,整合网络技术的应用,即将一个HTML网页文件中的文本与图片网址进行分离,并将文本与图片用不同文件夹分开保持;利用数据后台下载的功能;

3)可以借鉴的部分章节内容,第12章可以让你的APP界面变得更美观;第14章展示了一个大型的工程,可以学习下多个功能怎样在一个工程里体现;

4. 其它要求

1)构建的APP要格式工整,美观;

2)实验报告中需要有功能的描述、实验结果的截屏图像及详细说明;结果展示要具体,图文交叉解释;代码与文本重点要突出;

3)也欢迎采用课程后续章节的知识点完成本次大作业,如果实现的功能言之合理,会考虑酌情加分;

4)每位同学在最后一次课都需要上台报告,并且最好能现场演示APP的功能等,没上台报告的同学分数会受一定的影响;

5)报告由个人独立完成。

5. 评分标准

  1. APP协议完成度高,与参考APP有一定的相似度,功能完善、丰富。能实现活动的编写、自定义用户界面的开发、碎片开发、广播机制、数据持久化与共享技术、网络技术、后台服务的应用等。
    -------------(60分)
  2. 模拟APP结构合理,代码规范,界面美观易用。项目报告撰写规范、美观整齐,内容详实且能准备描述项目内容和设计思想、原理、框架等,项目报告要求5号字、除前两页外A4版面不低于10页的长度。
    -------------(15分)
  3. 提供程序源代码和可执行程序(或安装程序);报告文档采用单独的word文档,项目所有代码(不是整个工程文件,应该总共不超过5M)在第17周之前打包作为附件进行上传blackboard系统;纸质版交到任课老师处。
    -------------(10分)
  4. 项目报告能够详细,准确的描述项目内容,并在最后一堂课有较好的展示效果。
    -------------(15分)

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

1. 实验亮点:

①通过数据持久化与共享技术使用SQLite完成了历史搜索的功能;

②通过调用API网络接口完成对垃圾类别的判断;

③完成创新性功能——图片识别和语音识别,以及对应权限的申请;

④使用广播系统对网络进行判断,给出有无网络的提示;

⑤代码文件进行分文件夹分层存储,代码有易懂的结构;

⑥通过动态绘图进行布局,对不同种设备进行自适应匹配;

⑦使用更精美的UI动画(例如加载动画);

⑧通过代码托管平台实现了程序的版本更新;

⑨通过链接APPStore实现了对APP进行评分的可行性;

⑩完成了更细节的优化(如连续两次返回才退出,并在第一次时提示再滑动一次以退出);

⑪通过使用本地创建的秘钥将文件打包成APK;

⑫所有代码都有大量的注释,方便又易懂;

⑬实现了各个Adapter与Json处理工具,极大方便了开发过程;

⑭通过使用Intent进行Activity传值完成垃圾详情的查看;

⑮使用Fragment完成垃圾详情的查看;

⑯通过Logcat完成对整个项目的调试;

⑰采用LinearLayout进行布局。

2. “我的垃圾分类APP”的构建过程及结果

(1)欢迎页面的实现:

在日常使用中,为了使用APP更美观,并给APP内部数据加载预留时间,应设置一欢迎界面。在本次大作业中也有体现。
通过自动计时两秒,两秒后实现Activity的自动跳转。

public class WelcomeActivity extends BaseActivity {  
 @Override  
 protected void onCreate(Bundle savedInstanceState) {  
     requestWindowFeature(Window.FEATURE_NO_TITLE);  
     super.onCreate(savedInstanceState);  
     setContentView(R.layout.activity_welcome);  
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {  
         getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);  
         getWindow().setStatusBarColor(Color.TRANSPARENT);  
     }  
     RxCountDown.countdown(2).doOnSubscribe(disposable -> {  
     }).subscribe(new Observer<Integer>() {  
         @Override  
         public void onSubscribe(Disposable d) {}  
  
         @Override  
         public void onNext(Integer integer) {}  
  
         @Override  
         public void onError(Throwable e) {}  
  
         @Override  
         public void onComplete() {  
             startActivity(new Intent(WelcomeActivity.this, HomeActivity.class));  
             finish();  
         }  
     });  
 }

采用了ConstraintLayout进行布局:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    tools:context=".ui.activity.WelcomeActivity">  
</android.support.constraint.ConstraintLayout>

(2)垃圾分类的实现:

为了实现垃圾分类,我使用了一种互联网技术——API接口,并通过接口与服务器的数据交互完成对垃圾分类的判断。实现分获取API数据,显示和查询详细三个连续操作,下面分开介绍。

①获取API数据:
private void loadData() {  
    String serachStr = mEtSearch.getText().toString();  
    if (StringUtils.isEmpty(serachStr)) {  
        showToast("请输入搜索内容", mRelTops);  
    } else {  
        if (OrcLoadUtils.isWifiProxy(RkApplication.getInstance())) {  
            showToast("请先关闭代理等", mRelTops);  
        } else {  
            showStatus(mViewLottie, new LoadAbstract(this));  
            OrcLoadUtils.searchName(serachStr).subscribe(o -> {  
                try {  
                    hideStatus(mViewLottie);  
                    TrashResultEntity entity = (TrashResultEntity) o;  
                    if (entity != null) {  
                        if (entity.Ok) {  
                            if (entity.Data != null && entity.Data.size() > 0) {  
                                String serachStr2 = mEtSearch.getText().toString();  
                                if (!StringUtils.isEmpty(serachStr2)) {  
                                    setRestData(entity.Data);  
                                }  
                            } else {  
                                loadDataTwo(serachStr);  
                            }  
                        } else {  
                            loadDataTwo(serachStr);  
                        }  
                    }  
                } catch (Exception e) {  
                    loadDataTwo(serachStr);  
                    e.printStackTrace();  
                }  
            });  
        }  
    }  
}

在获取API数据时,定义了TrashResultEntity类用以接受返回的结果,并将各个获取的数据写回到该类中。

②进行数据展示:

要显示数据即在对应的Adapter中写入数据即可,通过函数传参进入即可。此外,如果查询结果为空,则需要进行特判,并显示未查询到结果。

private void setRestData(List<TrashResultEntity.DataBean> data) {  
     mTypeRecycle.setVisibility(View.GONE);  
     mResAdapter.setHeaderView(headerView);  
     mResAdapter.setNewData(data);  
 }  
private void setRestData(String search, String message) {  
     if (StringUtils.isEmpty(message)) {  
         message = "未查询到结果";  
     }  
     List<TrashResultEntity.DataBean> list = new ArrayList<>();  
     TrashResultEntity.DataBean data = new TrashResultEntity.DataBean();  
     data.Kind = 1008;  
     data.msg = message;  
     data.Name = search;  
     list.add(data);  
     mTypeRecycle.setVisibility(View.GONE);  
     mResAdapter.setHeaderView(headerView);  
     mResAdapter.setNewData(list);  
 }
③查询详细信息:

此时只需判断传入的垃圾的种类,如果为合法种类,则通过intent传值打开对应的Activity即可。

private void openDetails(int type) {  
     if (type == 1 || type == 2 || type == 3 || type == 4) {  
         Intent intent = new Intent(this, RefuseDetailActivity.class);  
         intent.putExtra("type", type);  
         startActivity(intent);  
     }  
 }

(3)语音识别:

在本次大作业中,为了方便各种用户不同的数据输入需求,我引用百度语音来进行语音识别并进行垃圾分类。进行语音识别的顺序大致如下:

①首先申请录音权限:

如果获得了录音权限,则将百度语音就绪;若无法获得录音权限,则弹出提示“权限不足”。

private void openBdAudio() {  
     PermissionUtils.permission(Manifest.permission.RECORD_AUDIO,  
             Manifest.permission.ACCESS_NETWORK_STATE,  
             Manifest.permission.INTERNET,  
             Manifest.permission.READ_PHONE_STATE,  
             Manifest.permission.WRITE_EXTERNAL_STORAGE)  
             .callback(new PermissionUtils.FullCallback() {  
                 @Override  
                 public void onGranted(List<String> permissionsGranted) {  
                     mLottigSound.setVisibility(View.VISIBLE);  
                     mImSound.setVisibility(View.GONE);  
                     asr.send(SpeechConstant.ASR_START, "{}", null, 0, 0);  
                     mTvSoundTips.setText("按下结束说话");  
                 }  
  
                 @Override  
                 public void onDenied(List<String> permissionsDeniedForever, List<String> permissionsDenied) {  
                     LogUtils.e("权限不足");  
                     showToast("权限不足");  
                 }  
             }).request();  
 }
②判断服务器是否就绪,若就绪则可以获取用户语音输入:
if(name.equals(SpeechConstant.CALLBACK_EVENT_ASR_READY)){  
             // 引擎就绪,可以说话,一般在收到此事件后通过UI通知用户可以说话了  
             LogUtils.e("引擎就绪,可以说话,一般在收到此事件后通过UI通知用户可以说话了");  
             mLottigSound.setVisibility(View.VISIBLE);  
             mImSound.setVisibility(View.GONE);  
         }
③当用户语音输入时,要提示出开始录音与录音结束

此处判断是否识别结束,如果已经识别结束,则给出提示“识别结束”,并将按钮的文字重新写为“按下说话”。

if(name.equals(SpeechConstant.CALLBACK_EVENT_ASR_FINISH)){  
             // 识别结束  
             LogUtils.e("识别结束");  
             mLottigSound.setVisibility(View.GONE);  
             mTvSoundTips.setText("按下说话");  
             mImSound.setVisibility(View.VISIBLE);  
         }
④当用户输入完成时,及判断是否为有效输入

如果输入有效,则调用API对垃圾的类别进行判断。在判断数据是否有效输入时,可以判断数据长度不为空且字符数组长度大于0

if(name.equals(SpeechConstant.CALLBACK_EVENT_ASR_PARTIAL)){  
             LogUtils.e(params);  
             RecogResult recogResult = RecogResult.parseJson(params);  
             // 识别结果  
             String[] results = recogResult.getResultsRecognition();  
             if (null != results && results.length > 0) {  
                 mEtSearch.setText(results[0]);  
             }  
             //LogUtils.e(results);  
         }

(4)图片识别:

本次大作业中,额外更部署了图片识别功能,也通过调用对应API接口来完成图片的识别,并完成对识别出结果的垃圾分类。

①与语音识别相同,首先要申请权限,如果获得权限则接口就绪,否则弹出权限不足的提示。
private void opemCamera() {  
     PermissionUtils.permission(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA)  
             .callback(new PermissionUtils.FullCallback() {  
                 @Override  
                 public void onGranted(List<String> permissionsGranted) {  
                     PictureSelector.create(HomeActivity.this)  
                             .openGallery(PictureMimeType.ofImage())  
                             .maxSelectNum(1)  
                             .selectionMode(PictureConfig.SINGLE)  
                             .forResult(9);  
                 }  
  
                 @Override  
                 public void onDenied(List<String> permissionsDeniedForever, List<String> permissionsDenied) {  
                     LogUtils.e("权限不足");  
                     showToast("权限不足");  
                 }  
             }).request();  
 }
②获取上传的图片后,调用对应的API接口,然后获取返回数据,并进行垃圾种类的判断。
private void loadImage(String image) {  
     showStatus(mViewLottie, new LoadAbstract(this));  
     OrcLoadUtils.imageTAg(image).subscribe(o -> {  
         LogUtils.e(o.toString());  
         if (o != null && !StringUtils.isEmpty(o.toString())) {  
             mEtSearch.setText(o.toString());  
         } else {  
             hideStatus(mViewLottie);  
         }  
     });  
 }

(5)网络监测:

这里我使用了广播系统对网络进行判断,并在没有网络的时候toast出提示信息。

public class NetWorkStateReceiver extends BaseBroadcastReceiver {  
  
 @Override  
 public void onReceive(Context context, Intent intent, int flag) {  
     if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {  
         // 发送网络改变事件  
         ConnectivityManager manager = (ConnectivityManager) Utils.getApp().getSystemService(Context.CONNECTIVITY_SERVICE);  
         //没有网络链接的时候 activeNetwork=null  
         NetworkInfo activeNetwork = manager.getActiveNetworkInfo();  
         if (activeNetwork == null){  
            ToastUtils.showShort("没有网络");  
         }else {  
            //ToastUtils.showShort("有网络");  
         }  
     }  
 }

(6)版本更新

在我们使用其他APP时,经常会遇到需要版本更新的情况,在本次大作业中,也有类似的设计。在本大作业中,我通过使用蒲公英代码托管平台托管最新版本,并通过对应API接口完成版本号获取,并完成更新。

①首先定义获取更新类:
public static class DataBean {  
    public boolean isForce;//是否强制更新  
    public int version;//版本号  
    public String message;//更新信息  
    public String url;//API接口链接  
}
②更新检测(判断是否有更新版本):

此处即进行版本号判断,如果没有更新版本,则提示“你已经是最新版本”。否则调用APP更新类对APP进行更新。

public static void checkUpdateVersion(final BaseActivity context) {  
 JKX_API.getInstance().getAppVersion("", new Observer() {  
     @Override  
     public void onSubscribe(Disposable d) {  
     }  
     @Override  
     public void onNext(Object o) {  
         try {  
             VersionEntity versionEntity = (VersionEntity) o;  
             String newVersion = versionEntity.data.version + "";  
             boolean isShowUpdate = AppUpdateUtils.newInstance().isCanUpdateApp(newVersion, context);  
             SPUtils.getInstance().put("is_update", isShowUpdate);  
  
             if (isShowUpdate)  
                 AppUpdateUtils.newInstance().updateApp(context, versionEntity.data.version + "", versionEntity.data.isForce , versionEntity.data.message, versionEntity.data.url);  
             else  
                 context.showToast("你已经是最新版本");  
         } catch (Exception e) {  
             e.printStackTrace();  
         }  
     }  
     @Override  
     public void onError(Throwable e) {  
         LogUtils.e(e);  
         SPUtils.getInstance().put("is_update", false);  
     }  
     @Override  
     public void onComplete() {  
     }  
 });
③弹出更新对话框:

更新时需要弹出更新对话框让用户知晓,因此需要设计一个对话框,让用户明确即将进行版本更新,若为非强制更新,则让用户选择是否进行更新。

public void showDownLoadDialog(final BaseActivity activity, final boolean isForce, String message, final String url) {  
     final WeakReference<BaseActivity> activityWeak = new WeakReference<BaseActivity>(activity);  
     final IosPopupDialog dialog = new IosPopupDialog(activity);  
     if (isForce) {//如果是非强制更新则让用户可以选择更新或取消  
         dialog.setCanceledOnTouchOutside(false);  
         dialog.setCancelable(false);  
     } else {  
         dialog.setCanceledOnTouchOutside(false);  
         dialog.setCancelable(true);//取消使能  
     }  
  
     dialog.setTextColor(R.id.tv_title, Color.parseColor("#333333"));  
     dialog.setTextColor(R.id.tv_message, Color.parseColor("#333333"));  
     //弹出对话框  
     dialog.setTitle("新版本更新说明")  
             .setMessage(message)  
             .setPositiveButton("确定", true, new View.OnClickListener() {  
                 @Override  
                 public void onClick(View v) {  
                     downLoadApk(activityWeak, isForce, url);  
                 }  
             });  
     if (!isForce) {  
         dialog.setNegativeButton("稍后再说", true, new View.OnClickListener() {  
             @Override  
             public void onClick(View v) {  
                 dialog.dismiss();  
             }  
         });  
     }  
     dialog.show();  
 }

下载对话框为一个弹窗,布局使用LinearLayout进行布局。

移动端 用户与组件的交互数据分析_java_05

<TextView  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:text="正在下载"  
     android:textColor="@color/black"  
     android:textSize="16sp"/>  
  
 <TextView  
     android:id="@+id/tv_update_app_progress"  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:layout_marginTop="8dp"  
     android:text="0%"  
     android:textColor="@color/black"  
     android:textSize="14sp"/>  
  
 <me.android.materialprogressbar.MaterialProgressBar  
     android:id="@+id/mpb_update_app_progress"  
     style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal"  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content"  
     android:layout_marginLeft="8dp"  
     android:layout_marginRight="8dp"  
     android:layout_marginTop="8dp"  
     android:max="100"  
     app:mpb_progressBackgroundTint="#f0f0f5"  
     app:mpb_progressStyle="horizontal"  
     app:mpb_progressTint="@color/colorPrimary"/>
④开始下载更新文件:

此处通过API接口完成更新数据流的链接,不断从API中获取更新数据。对于非强制更新,需要在更新时赋予用户取消使能。此时可以重用②中弹出对话框的代码。
显示之后即可新建线程,对线程启用监听,获取下载状态。

baseDownloadTask = FileDownloader.getImpl()  
             .create(url)  
             .setPath(FileDownloadUtils.getDefaultSaveRootPath() + "/" + "download")  
             .setForceReDownload(true)  
             .setListener(new FileDownloadListener()

若未处于下载状态则提示“等待中”字样。

protected void pending(BaseDownloadTask task, int soFarBytes, int totalBytes) {  
    LogUtils.d("BaseDownloadTask:pending");  
}

若处于下载状态,则显示当前下载进度:

protected void progress(BaseDownloadTask task, int soFarBytes, int totalBytes) {  
	int percent = (int) ((double) soFarBytes / (double) totalBytes * 100);  
 	LogUtils.d("BaseDownloadTask:" + percent);  
 	bar.setProgress(percent);  
 	progress.setText(percent + "%");  
 }
⑤此外,如果发生错误或警告,需要提示用户取消
protected void error(BaseDownloadTask task, Throwable e) {  
    downDialog.dismiss();  
}   
protected void warn(BaseDownloadTask task) {  
    downDialog.dismiss();  
}

(7)调用商店进行评分

在日常中我们使用的APP都会包含评分功能,通过调用系统的APP商店进行评分。本大作业也实现了这个功能。

只需调用对应的url并加载对应的Activity即可。此处使用了Intent进行传值。

Intent i = new Intent(Intent.ACTION_VIEW);  
             i.setData(Uri.parse("market://details?id=" + AppUtils.getAppPackageName()));  
             i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  
             startActivity(i);

(8)退出提示:

在我们日常使用APP的过程中经常遇到,为了防止用户误操作,造成意外退出的情况,此时需要当用户在主页面上连续两次点击返回而不是一次才能进行退出操作。
为了实现这个操作,只需判断用户是否在连续两秒内连续点击回退按钮即可。

public boolean onKeyDown(int keyCode, KeyEvent event) {  
    if (keyCode == KeyEvent.KEYCODE_BACK) {  
        long time = System.currentTimeMillis();  
        if (time - firstTime > 2000) {  
            showToast("再按一次退出");  
            firstTime = time;  
            return true;  
        } else {  
            finish();  
        }  
    }  
    return super.onKeyDown(keyCode, event);  
}

(9)打包成APK:

为了方便发布本APP,需要将APP打包成APK的格式,此处需要使用数字秘钥,选择Build->Generate Signed Bundle/APK 并选择APK,点击Next。

移动端 用户与组件的交互数据分析_java_06


然后由于本地没有数字秘钥,可以创建一个,点击new按钮。并在弹出页面中输入对应的信息和密码(此处密码都为SZUDYH)后点击OK,即可创建。到对应目录下即可看到打包好的APK文件。

移动端 用户与组件的交互数据分析_数据_07

3. 请详细说明“我的垃圾分类APP”的功能、出现的关键问题及解决方案

(1)主要功能:

①登录预览页:

打开APP之后不会立即进入,而是会展示五秒封面之后再进入APP。

移动端 用户与组件的交互数据分析_移动端 用户与组件的交互数据分析_08

②主页:

移动端 用户与组件的交互数据分析_java_09

③语音输入

可以通过语音完成垃圾名的输入:

移动端 用户与组件的交互数据分析_安卓_10


移动端 用户与组件的交互数据分析_移动开发_11

④图片识别:

可以通过识别上传的图片来完成垃圾的识别

移动端 用户与组件的交互数据分析_安卓_12

⑤版本更新:

此处我通过代码托管平台进行托管,完成版本的更新。

移动端 用户与组件的交互数据分析_数据_13

⑥APP评价:

可以通过设置->评价APP链接APPStore对APP进行打分。

由于本APP未发布,故显示No Data

移动端 用户与组件的交互数据分析_安卓_14


移动端 用户与组件的交互数据分析_数据_15


移动端 用户与组件的交互数据分析_移动开发_16

⑦主页上的几个按键:

移动端 用户与组件的交互数据分析_移动开发_17


移动端 用户与组件的交互数据分析_安卓_18

(2)遇到的问题及解决办法:

①**Q:ViewGroup为什么不会调用onDraw

A: 正常情况下,我们重写LinearLayout的onDraw方法,它是不会被调用的下面将分析原因并做解释。**
在完成主页面功能栏的布局中,遇到了问题重写LinearLayout的onDraw方法时不会被调用,从而导致错误。

<com.test.demo.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  
    android:id="@+id/ll_absolute"  
  
    android:orientation="vertical"  
  
    android:layout_width="fill_parent"  
  
    android:layout_height="fill_parent"  
  
    android:background="#FF000000">  
  
</com.test.demo.MyLinearLayout>

大概的代码层次是,MyLinearLayout从LinearLayout派生出来,然后在程序中重载OnDraw。但是,onDraw不会被调用。我们可能会遇到这个问题:如果不给LinearLayout设置一个背景,系统是不会调用onDraw时,也就是说,我们重写的onDraw是不会调用的。当设置一个背景后,onDraw就会被调用。

造成这种现象的原因是继承自LinearLayout,而LinearLayout这是一个容器,ViewGroup本身并没有任何可画的东西,它是一个透明的控件,因些并不会触发onDraw,但是如果给LinearLayout设置一个背景色,其实这个背景色不管你设置成什么颜色,系统会认为LinearLayout上面有东西可画了,因此会调用onDraw方法。

我们可以仔细分析View的源码,它有一个方法View#draw(Canvas)方法,这里面有两个地方调用onDraw,它的条件都是:if (!dirtyOpaque) onDraw(canvas);

也就是说,如果dirtyOpaque是true的话,onDraw就不会调用,而dirtyOpaque的值的计算代码如下:

final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&  
             (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);

此外,View还提供了一个重要的方法:setWillNotDraw:

public void setWillNotDraw(boolean willNotDraw) {  
     setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);  
 }

通过查阅资料,得知如果想重写onDraw,应调用这个方法来清除flag,所以如果想要重写LinearLayout的onDraw,可以在其构造方法中调用setWillNotDraw方法。 在ViewGroup初始他时,它调用了一个私有方法:initViewGroup,其中setFlags(WILL_NOT_DRAW, DRAW_MASK); 相当于调用了setWillNotDraw(true),因此对于ViewGroup,它就认为是透明的了。
如果我们想要重写onDraw,就需要调用setWillNotDraw(false)

综上所述:
i.ViewGroup默认情况下,会被设置成WILL_NOT_DRAW,这是从性能考虑,这样一来,onDraw就不会被调用了。
ii.如果要重写一个ViweGroup的onDraw方法,有两种方法:在构造函数里面,给其设置一个颜色,如#00000000或在构造函数里面,调用setWillNotDraw(false),去掉其WILL_NOT_DRAW flag。

②加载条死循环

我的透明进度条原理是不断重绘一个背景图片,然后使用Matrix旋转图片,原先的代码是死循环,代码如下:

protected Void doInBackground(Void... params) {  
    while( true ) {  
        try {  
            Thread.sleep(100);  
        } catch(InterruptedException e) {  
            e.printStackTrace();  
        }  
        publishProgress();  
    }  
    return null;  
}

这样就有一个bug,因为是死循环,当ProgressDialog被dismiss掉之后这个循环并不会销毁,而是一直在循环,这样造成的一个bug就是使用了透明进度条之后侧滑菜单点击就没有反应了,并且,透明进度条在Activity下次启动并不会有旋转的效果,这里修改的办法是将这个while(true)改成判断while( mLoop ) 然后重写ProgressDialog的dismiss()方法:

public void dismiss() {  
     super.dismiss();  
     mProgressView.setLoop(false);  
 }
③使用Logcat进行debug

Logcat 是一个命令行工具,用于转储系统消息日志,包括设备抛出错误时的堆栈轨迹,以及从应用使用 Log 类写入的消息。

Android 日志记录系统是系统进程 logd 维护的一组结构化环形缓冲区。这组可用的缓冲区是固定的,并由系统定义。最相关的缓冲区为:main(用于存储大多数应用日志)、system(用于存储源自 Android 操作系统的消息)和 crash(用于存储崩溃日志)。每个日志条目都包含一个优先级(VERBOSE、DEBUG、INFO、WARNING、ERROR 或 FATAL)、一个标识日志来源的标记以及实际日志消息。

日志记录系统的主接口是共享库 liblog 及其头文件 <android/log.h>。所有语言特定的日志记录工具最终都会调用函数 __android_log_write。默认情况下,它会调用函数 __android_log_logd_logger,该函数使用套接字将日志条目发送到 logd。从 API 级别 30 开始,可通过调用 __android_set_log_writer 更改日志记录函数。
通过使用Logcat可以比较方便的获得程序的运行情况

例如如下情况:

移动端 用户与组件的交互数据分析_安卓_19


移动端 用户与组件的交互数据分析_数据_20

④ScrollView和ListView两个View都有滚动的效果,在嵌套使用时起冲突的问题

在scrollview里面嵌套了一个listview ,通过设置一个方法设置了listview的高度 现在的情况就是进到这个界面的时候看到的不是最上面 而是中间 ,该问题的解决办法为:
mScrollView.smoothScrollTo(0,20);
如此以上代码还是无效, 在代码里去掉listview的焦点 lv.setFocusable(false)即可。
或与EditText一样,在父元素的属性下面下下面这两行即可,不用代码设置。亲测可用。
android:focusableInTouchMode=“true”
android:focusable=“true”

/**   
  * 重新计算ListView的高度,解决ScrollView和ListView两个View都有滚动的效果,在嵌套使用时起冲突的问题   
  * @param listView   
  */    
 public void setListViewHeight(ListView listView) {      
           
     // 获取ListView对应的Adapter      
       
     ListAdapter listAdapter = listView.getAdapter();      
       
     if (listAdapter == null) {      
         return;      
     }      
     int totalHeight = 0;      
     for (int i = 0, len = listAdapter.getCount(); i < len; i++) { // listAdapter.getCount()返回数据项的数目      
         View listItem = listAdapter.getView(i, null, listView);      
         listItem.measure(0, 0); // 计算子项View 的宽高      
         totalHeight += listItem.getMeasuredHeight(); // 统计所有子项的总高度      
     }      
       
     ViewGroup.LayoutParams params = listView.getLayoutParams();      
     params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));      
     listView.setLayoutParams(params);      
 }
⑤Theme.AppCompat.Light报错

在styles.xml中<style name=“AppBaseTheme” parent=“Theme.AppCompat.Light”>提示如下错误,这是版本问题。
error: Error retrieving parent for item: No resource found that matches the given name ‘Theme. AppCompat. Light’.
解决方法:
打开任意一个.java文件,输入android.R.style.这时你会看到一个提示列表,仔细看看,再对比一下,看哪个与原有的最接近就选择哪个。用将下划线替换成"."就可了。
将<style name=“AppBaseTheme” parent=“Theme.AppCompat.Light”>改为<style name=“AppBaseTheme” parent=“android:Theme.Light”>同理,将<style name=“AppBaseTheme” parent=“Theme.AppCompat.Light.DarkActionBar”>改为<style name=“AppBaseTheme” parent=“android:Theme.Holo.Light.DarkActionBar”>。

⑥偶发AndroidStudio进入界面卡死,无法进入

经过上网查阅资料后,发现是如下几个原因,并附上解决办法:
如果是因为内存不足:修改studio64.exe.vmoptions studio.exe.vmoptions 2个文件,将文件内分配内存调大即可。
或者是加载时间过长导致一直处于加载状态,此时只需打开AS安装目录下的bin内的idea.properties文件,并在最后加上disable.android.first.run=true

三、实验总结

本次大作业中,我使用了数据持久化与共享技术,网络技术——API接口,安卓权限申请,广播系统,安卓动态绘图,Intent传值完成Activity交互,Fragment碎片布局,Logcat调试,LinearLayout布局。并自学了如何将代码打包成APK,参考网上资料学习了怎么对APP完成版本更新与调用商店进行评分。通过自学第14章,对工程代码进行分层次分结构存储。通过学习第12章也尝试使用了一些精美的UI动画使得APP更美观。
此外,我也自学了Java的相关知识,如线程,单例等等,这些都是我之前没用过也没尝试过的。本次大作业极大的提高的我的编程能力与实际应用能力。由于疫情在家期间自学了Java,在本课程的每次实验乃至本次大作业中,并没有遇到比较多的问题。
通过一学期对《移动设备交互应用》这门课的学习,我的Java代码技术与自学能力得到了很大提升,我也学会了如何将一些点子转化为代码并进行实现。