Android
Android - 应用前后台切换监听
最近工作中遇到了这么一个需求:如何实现 Android 应用前后台切换的监听?
解决方案笔记。
iOS 的情况
iOS 内边是可以实现的,UIApplicationDelegate
给了一个回调监听:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
}
我保留了系统注释。一个iOS应用周期,大概的流程是这样的。
Android 一种简单和优雅的方式实现下拉刷新和加载更多
老生常谈的问题,但是发现很多人仍然将其视为难题。
很多实现也很复杂,有人甚至做了一个 SuperRefreshLayout 或者 SuperListView 或者 SuperRecyclerView 来实现。
其实不需要 Super,使用很少的简介的代码也可以实现。
最近做了个总结,尝试给出最佳实践。上面是效果图,支持 ListView 和 RecyclerView。
相关代码在这里:https://github.com/TakWolf/Android-RefreshAndLoadMore-Demo
这个方案来源于我自己的开发实践,个人认为已接近最佳实践,简单且优雅。
要注意的是,这篇文章介绍的是如何用现有资源快速实现一个下拉刷新和加载更多分页效果,
而不是如何去实现或者自定义一个下拉刷新控件。那是另外一个话题,咱们改天再聊。
准备工作
我们的原则是,尽可能使用现成的资源去实现。
即便如此,我们仍然使用了下面的第三方依赖,但是注意,这些依赖都不是必须的:
一个可以兼容低版本的 Material Design 风格进度条样式。我希望可以保证所有版本的 Android 都要良好的 Material Design 体验。
这不是必须的,你可以替换为默认的进度条控件或者你自己的控件,除了样式之外,不会有其他问题。
2.Android-HeaderAndFooterRecyclerView
这是我写的另外一个组件,它可以扩充 RecyclerView,让其支持 HeaderView 和 FooterView。我们在实现加载更多的时候需要这个特性。
这个组件使用非常简单,替换默认的 RecyclerView 就可以了。接口与 ListView 添加头部的方式类似。不需要修改其他任何东西(例如你的业务Adapter)。
仅此而已,非侵入式,没有额外的东西。
这不是必须的,你可以有你自己的方式实现这个功能。这不影响我们整篇文章的思路。但是我很推荐你去试试它,也许你会觉得不错呢。
通过注解的方式快速实现控件绑定。
这不是必须的,你可以使用 findViewById(),效果是一样的。不过我推荐你去看看,真的很方便。
基本思路
下拉刷新比较容易实现,因为 Android 官方的 support 组件中就实现了一个下拉刷新组件:SwipeRefreshLayout
支持 ListView 和 RecyclerView,符合 Material Design,使用简单,性能良好。
港真,除非你遇到一个变态的产品经理,否则有啥理由不使用这个呢?
下拉刷新的实现,大家的方式分歧比较大,很多人尝试在父布局上面下功夫。
我个人觉得最简单的实现是在列表的最后添加一个 FooterView,用它来显示加载动画。同时监听列表滚动,检测滑动到底部的时候,触发加载动作。
这样的好处是,不在需要复杂的去实现一个自定义控件(包括去实现那些复杂的手势处理),也没有一些特殊情况下的问题(比如首页加载不足一屏,我们下面会讲)。
Android - 关于 Log.wtf()
Android SDK 的日志接口中,有这么一个函数:
android.util.Log.wtf();
我一直认为,这个是 “What the fuck” 的缩写,用于出现了不可能,无法挽救的错误的场景。
我甚至还觉得谷歌的工程师还蛮有意思的,诙谐幽默。
直到我今天偶然点进去了这个方法,看到了文档注释:
What a Terrible Failure: Report a condition that should never happen.
The error will always be logged at level ASSERT with the call stack.
Depending on system configuration, a report may be added to the
{@link android.os.DropBoxManager} and/or the process may be terminated
immediately with an error dialog.
原来 wtf
是 “What a Terrible Failure” 的意思……
就这么个事,挺有趣的,水了一篇博客。
Android 的 support-v4 扩展库的 minSdkVersion=9 !是笔误还是故意为之?
今天更新了 Android Support Repository,相应的 android-support-v4 也升级到了 24.2.1 但是我的项目却报了如下的错误:
Error:Execution failed for task ':fragmentswitcher:processDebugAndroidTestManifest'.
> Manifest merger failed : uses-sdk:minSdkVersion 4 cannot be smaller than version 9 declared in library [com.android.support:support-v4:24.2.1] ~\Android-FragmentSwitcher\fragmentswitcher\build\intermediates\exploded-aar\com.android.support\support-v4\24.2.1\AndroidManifest.xml
Suggestion: use tools:overrideLibrary="android.support.v4" to force usage
这个错误的意思是,你的项目的 minSdkVersion=4,而 android-support-v4 要求他至少为9
看到这个错误,我其实是一脸懵逼的。
Android Support Repository 包含了很多库,很多都是按照 vX 结尾的,这个其实表示这个库的 API 兼容到的最低版本。 举个例子: cardview-v7,兼容到 API7 support-v13,兼容到 API13 也就是说,support v4 的意思就是,把一些兼容性组件,兼容到 API4 的级别上。
那么,你把 minSdkVersion 设置为9,那还叫啥 v4 啊?
心塞。。。
Android Support Repository 是安装到本地的 位置在:%ANDROID_SDK_HOME%/sdk/extras/android/m2repository/com/android/support
Android Studio 启动卡在 “fetching Android sdk compoment information”
由于国内墙的原因,无法连接到 Google 的更新服务器。
解决方法: 进入刚安装的 Android Studio 目录下的 bin 目录。 找到 idea.properties 文件,用文本编辑器打开。 在idea.properties文件末尾添加一行:
disable.android.first.run=true ,然后保存文件。关闭Android Studio后重新启动。
PJSIP 编译 Android 平台,报错:cannot locate symbol "rand" referenced by "libpjsua.so"
最近公司要实现一个IP电话的功能,选择了PJSIP这个框架。 服务端部分团队大牛已经搞完了,Android 这块还要实现一个 SIP 客户端。 安装官方文档编译Hello World,整个过程就是一个字:坑爹。
最开始是在 Windows 环境上编译,由于编译依赖于 Bash 命令以及 swig,还有坑爹的 Windows 和 Unix 换行符转换的问题,是在解决无力。 更换到 Ubuntu 之后编译通过了,在运行时报错:
cannot locate symbol "rand" referenced by "libpjsua.so"
但奇怪的是在 Android5.0 上运行却正常。 Google 上说是因为使用了64位 ndk 的原因,遂更换为32为 Ubuntu,问题仍未解决。
最后将 ndk 从 10r 更换到 9r,编译和运行都通过了。 猜测问题出现在 ndk 10r 的兼容性上。
吐槽一下 Android5.0 正式版有太多的兼容性问题。 ndk 10r 是匹配到 Android5.0 而推送的,估计也有兼容性问题。
经验和教训,简而言之就是以下几句话:
-
别用 Windows 编译
-
别用 Ubuntu 64位,使用32位,包括 ndk
-
别用 ndk 版本号高于 r10,使用 r9 以下版本
我编译成功的环境配置:
-
Ubuntu-14.04.1-LTS-i386
-
PJSIP-2.3
-
ndk-r9d-linux-x86
Android 九宫格锁屏布局 - Android Lock9View
最近项目要实现一个类似支付宝类似需求的锁屏需求,找的一些第三方实现或多或少都有些问题。 这个功能实现起来不是很困难,于是就参看了一些例子,自己做了一个。
源代码已经托管于Github:Android-Lock9View
思路
创建一个自定义 View 继承自 ViewGroup,用9个子 View 表现节点,在 onDraw 中绘制线段。
代码
废话不多说,Lock9View.java
/* * Copyright 2015-2016 TakWolf * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.takwolf.android.lock9; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Pair; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.List; public class Lock9View extends ViewGroup { /** * 节点相关定义 */ private List<Pair<NodeView, NodeView>> lineList = new ArrayList<Pair<NodeView,NodeView>>(); // 已经连线的节点链表 private NodeView currentNode; // 最近一个点亮的节点,null表示还没有点亮任何节点 private float x; // 当前手指坐标x private float y; // 当前手指坐标y /** * 自定义属性列表 */ private Drawable nodeSrc; private Drawable nodeOnSrc; private int lineColor; private float lineWidth; private float padding; // 内边距 private float spacing; // 节点间隔距离 /** * 画线用的画笔 */ private Paint paint; /** * 密码构建器 */ private StringBuilder passwordBuilder = new StringBuilder(); /** * 结果回调监听器接口 */ private CallBack callBack; public interface CallBack { public void onFinish(String password); } public void setCallBack(CallBack callBack) { this.callBack = callBack; } /** * 构造函数 */ public Lock9View(Context context) { this(context, null); } public Lock9View(Context context, AttributeSet attrs) { this(context, attrs, 0); } public Lock9View(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public Lock9View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr); // TODO 这个构造函数在api21以上版本才可用 initFromAttributes(attrs, defStyleAttr); } /** * 初始化 */ private void initFromAttributes(AttributeSet attrs, int defStyleAttr) { // 获取定义的属性 final TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.Lock9View, defStyleAttr, 0); nodeSrc = a.getDrawable(R.styleable.Lock9View_lock9_nodeSrc); nodeOnSrc = a.getDrawable(R.styleable.Lock9View_lock9_nodeOnSrc); lineColor = a.getColor(R.styleable.Lock9View_lock9_lineColor, Color.argb(0, 0, 0, 0)); lineWidth = a.getDimension(R.styleable.Lock9View_lock9_lineWidth, 0); padding = a.getDimension(R.styleable.Lock9View_lock9_padding, 0); spacing = a.getDimension(R.styleable.Lock9View_lock9_spacing, 0); a.recycle(); // 初始化画笔 paint = new Paint(Paint.DITHER_FLAG); paint.setStyle(Style.STROKE); paint.setStrokeWidth(lineWidth); paint.setColor(lineColor); paint.setAntiAlias(true); // 抗锯齿 // 构建node for (int n = 0; n < 9; n++) { NodeView node = new NodeView(getContext(), n + 1); addView(node); } // 清除FLAG,否则 onDraw() 不会调用,原因是 ViewGroup 默认透明背景不需要调用 onDraw() setWillNotDraw(false); } /** * TODO 我们让高度等于宽度 - 这么使用不清楚是否正确 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(widthMeasureSpec, widthMeasureSpec); } /** * 在这里进行node的布局 */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (changed) { int nodeWidth = (int) ((right - left - padding * 2 - spacing * 2) / 3); for (int n = 0; n < 9; n++) { NodeView node = (NodeView) getChildAt(n); // 获取3*3宫格内坐标 int row = n / 3; int col = n % 3; // 计算实际的坐标,要包括内边距和分割边距 int l = (int) (padding + col * (nodeWidth + spacing)); int t = (int) (padding + row * (nodeWidth + spacing)); int r = l + nodeWidth; int b = t + nodeWidth; node.layout(l, t, r, b); } } } /** * 在这里处理手势 */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: x = event.getX(); // 这里要实时记录手指的坐标 y = event.getY(); NodeView nodeAt = getNodeAt(x, y); if (currentNode == null) { // 之前没有点 if (nodeAt != null) { // 第一个点 currentNode = nodeAt; currentNode.setHighLighted(true); passwordBuilder.append(currentNode.getNum()); invalidate(); // 通知重绘 } } else { // 之前有点-所以怎么样都要重绘 if (nodeAt != null && !nodeAt.isHighLighted()) { // 当前碰触了新点 nodeAt.setHighLighted(true); Pair<NodeView, NodeView> pair = new Pair<NodeView, NodeView>(currentNode, nodeAt); lineList.add(pair); // 赋值当前的node currentNode = nodeAt; passwordBuilder.append(currentNode.getNum()); } invalidate(); // 通知重绘 } break; case MotionEvent.ACTION_UP: if (passwordBuilder.length() > 0) { // 有触摸点 // 回调结果 if (callBack != null) { callBack.onFinish(passwordBuilder.toString()); } // 清空状态 lineList.clear(); currentNode = null; passwordBuilder.setLength(0); // 清除高亮 for (int n = 0; n < getChildCount(); n++) { NodeView node = (NodeView) getChildAt(n); node.setHighLighted(false); } // 通知重绘 invalidate(); } break; } return true; } /** * 系统绘制回调-主要绘制连线 */ @Override protected void onDraw(Canvas canvas) { // 先绘制已有的连线 for (Pair<NodeView, NodeView> pair : lineList) { canvas.drawLine(pair.first.getCenterX(), pair.first.getCenterY(), pair.second.getCenterX(), pair.second.getCenterY(), paint); } // 如果已经有点亮的点,则在点亮点和手指位置之间绘制连线 if (currentNode != null) { canvas.drawLine(currentNode.getCenterX(), currentNode.getCenterY(), x, y, paint); } } /** * 获取给定坐标点的Node,返回null表示当前手指在两个Node之间 */ private NodeView getNodeAt(float x, float y) { for (int n = 0; n < getChildCount(); n++) { NodeView node = (NodeView) getChildAt(n); if (!(x >= node.getLeft() && x < node.getRight())) { continue; } if (!(y >= node.getTop() && y < node.getBottom())) { continue; } return node; } return null; } /** * 结点描述类 */ private class NodeView extends View { private int num; private boolean highLighted = false; public NodeView(Context context, int num) { super(context); this.num = num; setBackgroundDrawable(nodeSrc); } public boolean isHighLighted() { return highLighted; } public void setHighLighted(boolean highLighted) { if (this.highLighted != highLighted) { this.highLighted = highLighted; setBackgroundDrawable(highLighted ? nodeOnSrc : nodeSrc); } } public int getCenterX() { return (getLeft() + getRight()) / 2; } public int getCenterY() { return (getTop() + getBottom()) / 2; } public int getNum() { return num; } } }
需要注意的几点:
Android 跳转到应用市场详细信息页面
要实现的功能很简单,用户点击“给应用评分”按钮,就会跳转到应用商店的详细信息页面。 或者点击“更多应用”按钮跳转到商店搜索页面搜索开发者的相关应用。
原理十分简单,构建一个
ACTION_VIEW
标记的Intent
,并给一个如下结构的 Uri 即可:"market://details?id=" + getPackageName() //商店中使用包名来唯一标识区分应用
在 Android 平台上,正常情况下手机中的应用商店应该是 Google Play 但是由于各种你懂我也懂的原因,国内基本上无法使用 Google Play 服务。 好在广泛的第三方应用市场大多都实现了这个接口。
代码注释很详细:
//这里开始执行一个应用市场跳转逻辑,默认this为Context上下文对象 Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("market://details?id=" + getPackageName())); //跳转到应用市场,非Google Play市场一般情况也实现了这个接口 //存在手机里没安装应用市场的情况,跳转会包异常,做一个接收判断 if (intent.resolveActivity(getPackageManager()) != null) { //可以接收 startActivity(intent); } else { //没有应用市场,我们通过浏览器跳转到Google Play intent.setData(Uri.parse("https://play.google.com/store/apps/details?id=" + getPackageName())); //这里存在一个极端情况就是有些用户浏览器也没有,再判断一次 if (intent.resolveActivity(getPackageManager()) != null) { //有浏览器 startActivity(intent); } else { //天哪,这还是智能手机吗? Toast.makeText(this, "天啊,您没安装应用市场,连浏览器也没有,您买个手机干啥?", Toast.LENGTH_SHORT).show(); } }
需要注意的就是,如果界面跳转失败,会抛出异常,因此能否跳转需要进行判断。
根据以上,同理使用以下Uri进行替换:
Uri.parse("market://search?q=pub:Author Name"); //跳转到商店搜索界面,并搜索开发者姓名 Uri.parse("market://search?q=Keyword"); //跳转到商店搜索界面,并搜索关键词
参考链接: http://stackoverflow.com/questions/4702204/android-market-detailsid-not-working-for-app
Android 录音生成 m4a 格式音频
一篇博文,十分详细,记录
http://blog.csdn.net/yegongheng/article/details/40624267
Android 实现视频播放
笔记,实现 Android 视频播放。
提到视频播放,有这么几种方法。
1.调用系统播放器
Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath()+"/video.mp4"); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, "video/mp4"); //这句不要忘记,表明资源是视频类型 startActivity(intent);
2.使用MediaPlayer和SurfaceView
说实话,这种方式太过麻烦,代码就不粘贴了。其实还有更简单的下面的第三种方法。
3.使用VideoView
这个本身就是为了播放视频而设计的控件。 通过查看基础树我们发现,其实他就是继承了
SurfaceView
并且实现了MediaPlayerControl
实质是第二种方式的一个封装,而使用起来要方便很多,视频在 start 调用之后自动加载,你也不需要去手动释放资源,控件都帮你处理了。一个非常简单的例子:
videoView = (VideoView) findViewById(R.id.video_view); videoView.setMediaController(new MediaController(this)); Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath()+"/video.mp4"); videoView.setVideoURI(uri); videoView.start();
上面代码中的
setMediaController
可以添加一个媒体控制器。 这是一个默认实现的控件,包括进度,暂停神马的,如果没有特殊的要求,功能基本也就够用了。 当然,你也可以自己来实现。 VideoView 本身也提供媒体控制接口和监听器。Android系统默认支持的视频格式如下:
//Video addFileType("MP4", FILE_TYPE_MP4, "video/mp4"); addFileType("M4V", FILE_TYPE_M4V, "video/mp4"); addFileType("3GP", FILE_TYPE_3GPP, "video/3gpp"); addFileType("3GPP", FILE_TYPE_3GPP, "video/3gpp"); addFileType("3G2", FILE_TYPE_3GPP2, "video/3gpp2"); addFileType("3GPP2", FILE_TYPE_3GPP2, "video/3gpp2"); addFileType("WMV", FILE_TYPE_WMV, "video/x-ms-wmv");
这里还有一个问题要特殊说明一下: VideoView 加载视频需要通过 Uri 或者 Path。 上面的代码是从 SDCard 中加载视频的,Uri 的构筑方式是:
Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath()+"/video.mp4");
当然,有的时候我们可能并不是要制作一个视频播放器,而是仅仅加载一些应用内置视频,做些产品或者功能演示神马的。 我们的第一个思路是将视频文件放到 assets 资源目录下。 实际编码中我们发现 AssetManager 竟然不能够直接获取 Path,也无法直接构筑 Uri。 我们按照以往的经验:
辽工大教务在线 Android 客户端 v1.2.6 发布
辽工大教务在线 Android 客户端,v1.2.6 已更新。 全新的界面设计,更简洁更扁平。 课程表支持本地缓存。 增加意见反馈功能,随时随地进行吐槽。
低于 1.2.0 版本的应用安装会出现签名冲突错误,请先卸载旧版本再进行安装即可。
下载地址:
在应用市场渠道下载的同学们,记得给好评哟~~