我的第一个安卓应用终于也有了APP内安装更新的功能(赶上末班车了吗),记录一些关键点,方方面面的。
由于没有服务器,这两个核心功能可以托管到一些比较好的平台。检测我用的是蒲公英分发(内测阶段),下载用的则是无限蓝云(hhh)。如果蒲公英过审了也可以只用一个,不知道难度大不大……
高版本需要fileprovider,其实不用的话直接vmpolicy微调一下也行。
两个关键点都需要在清单文件中处理:1、定义 fileprovider,2:声明权限(否则没有反应)。
manifest:
其中 android:resource="@xml/file_paths"需要提供一个清单文件res/xml/file_paths.xml,可如下:
这样,需要下载安装包到: 外部储存->临时文件夹( /storage/0/Android/data/包名/cache )中的 apks 文件夹: File target = new File(getExternalCacheDir(), "apks/"+versionName); ,才能被 FileProvider 识别。
Java 调用 :
private void startUpdateInstall(File target) {try {Intent intent = new Intent(Intent.ACTION_VIEW);File file = target;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {Uri apkUri = FileProvider.getUriForFile(PDICMainActivity.this, "包名.fileprovider", file);intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);intent.setDataAndType(apkUri, "application/vnd.android.package-archive");} else {intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);Uri uri = Uri.fromFile(file);intent.setDataAndType(uri, "application/vnd.android.package-archive");}startActivity(intent);} catch (Exception e) {CMN.debug(e);}}
FileProvider还可以包含更多文件夹, 参考 Stack Overflow - ‘Failed to find configured root’:
--> Context.getFilesDir() --> Context.getCacheDir() --> Environment.getExternalStorageDirectory() --> Context.getExternalFilesDir(String) --> Context.getExternalCacheDir() --> Context.getExternalMediaDirs()每一个版本都可以提炼一些简短的介绍,然后在检测更新的时候一起获取,显示出来。
建议用Markdown格式写更新日志,Markdown 之简洁优雅足以胜任一定的生产力。
有许多开源组件可以展示Markdown,比如io.noties.markwon或者org.commonmark,前者体积较大、更加完善,后者更简单,但只是转换为html,需要再配合Html.fromHtml转换Spannable成才行
而安卓的Textview虽然支持各种图文混排,但有一些bug,比如设置linkedmovement后、再设置文本可选,会导致滚动点击时随机崩溃。
两个办法解决,一是自定义textview,try-catch包绕一些会崩溃的方法如dispatchTouchEvent(不推荐)。二是自定义触摸监听器,在onTouch中自行调用ClickableSpan、UrlSpan、LinkSpan等的的点击方式。
// 自定义触摸监听器,手动调用 ClickableSpan
TextView tv = (TextView) v;
CharSequence text = tv.getText();
if(text instanceof Spannable) {Spannable span = (Spannable) text;int x = 触摸位置_X;int y = 触摸位置_Y;x -= tv.getTotalPaddingLeft();y -= tv.getTotalPaddingTop();x += tv.getScrollX();y += tv.getScrollY();Layout layout = tv.getLayout();if(layout!=null) {int line = layout.getLineForVertical(y);int off = layout.getOffsetForHorizontal(line, x);ClickableSpan[] link = span.getSpans(off, off, ClickableSpan.class);if (link.length > 0) {touching = ……// 记录 link[0], 然后在ACTION_UP或onClick时,调用点击监听器}}
}@Override
public void onClick(View v) {ClickableSpan touching = getTouchingSpan(v);if (touching!=null) {TextView widget = (TextView) v;Spannable span = (Spannable) widget.getText();if (clickInterceptor != null && clickInterceptor.onClick(widget, touching)) {// intentionally blank} else {touching.onClick(v);}Selection.setSelection(span,span.getSpanStart(touching),span.getSpanEnd(touching));}
}
这里进度条参考的是百度第一个博客里的:给progressbar设置drawable和自定义progressbar:
purpose_drawable.xml
细看,里面是两层的layerdrawable,第一层是底色,另一层id/progress则是进度的颜色,不过里面的渐变色似乎没有用到啊。
然后布局代码里给seekbar添加android:progressDrawable:@drawable/purpose_drawable属性即可,非常快啊,简直不讲武德。
大佬说得好,不仅仅要创造 progress,还要创造 purpose,以后就叫做 purposeBar 吧。
下载之时,我直接用进度条替换了对话框底部的其中一个按钮。这种替换操作用着很爽,我甚至提炼了一个方法 ViewUtils.replaceView ,一系列操作原生视图的方法 ……
public static View replaceView(View viewToAdd, View viewToRemove) {return replaceView(viewToAdd, viewToRemove, true);
}public static View replaceView(View viewToAdd, View viewToRemove, boolean layoutParams) {ViewGroup.LayoutParams lp = viewToRemove.getLayoutParams();ViewGroup vg = (ViewGroup) viewToRemove.getParent();if(vg!=null) {int idx = vg.indexOfChild(viewToRemove);removeView(viewToAdd);if (layoutParams) {vg.addView(viewToAdd, idx, lp);} else {vg.addView(viewToAdd, idx);}removeView(viewToRemove);}return viewToAdd;
}public static boolean removeView(View viewToRemove) {return removeIfParentBeOrNotBe(viewToRemove, null, false);
}public static boolean removeIfParentBeOrNotBe(View view, ViewGroup parent, boolean tobe) {if(view!=null) {ViewParent svp = view.getParent();if((parent!=svp) ^ tobe) {if(svp!=null) {((ViewGroup)svp).removeView(view);//CMN.Log("removing from...", svp, view.getParent(), view);return view.getParent()==null;}return true;}}return false;
}
效果图:
