这是自己在项目当中踩过的坑而总结整理的笔记,仅供自己。署名android developer
基础知识
此部分包含android相关的基础部分,而且是容易忽视的基础,平时很少用到但用时却不知道的。
1 Android launchMode
Task是一个具有栈结构的对象,一个Task可以管理多个Activity,启动一个应用,也就创建一个与之对应的task。Activity一共有以下四种launchMode:
- standard
- singleTop
- singleTask
- singleInstance
[图片上传失败...(image-a1184e-1556638375279)]
[图片上传失败...(image-398128-1556638375279)]
[图片上传失败...(image-eea1a2-1556638375279)]
每次跳转系统都会在task中生成一个新的FirstActivity实例,并且放于栈结构的顶部,当我们按下后退键时,才能看到原来的FirstActivity实例。standard启动模式,不管有没有已存在的实例,都生成新的实例。
[图片上传失败...(image-d1ace4-1556638375279)]
[图片上传失败...(image-3ddbdc-1556638375279)]
[图片上传失败...(image-9b51ba-1556638375279)]
三个序列号是相同的,也就是说使用的都是同一个FirstActivity实例;如果按一下后退键,程序立即退出,说明当前栈结构中只有一个Activity实例。跳转时系统会先在栈结构中寻找是否有一个FirstActivity实例正位于栈顶,如果有则不再生成新的,而是直接使用。
singleTop启动模式,如果发现有对应的Activity实例正位于栈顶,则重复利用,不再生成新的实例。
singleTask模式,如果发现有对应的Activity实例,则使此Activity实例之上的其他Activity实例统统出栈,使此Activity实例成为栈顶对象,显示到幕前。
singleInstance启动模式可能是最复杂的一种模式,为了帮助大家理解,我举一个例子,假如我们有一个share应用,其中的ShareActivity是入口Activity,也是可供其他应用调用的Activity,我们把这个Activity的启动模式设置为singleInstance,然后在其他应用中调用。我们编辑ShareActivity的配置:
<activity android:name=".ShareActivity" android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SINGLE_INSTANCE_SHARE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
然后我们在其他应用中这样启动该Activity:
Intent intent = new Intent("android.intent.action.SINGLE_INSTANCE_SHARE");
startActivity(intent);
一般的登陆界面LoginActivity设计为
android:launchMode="singleTask"
Flags模式:
//如果activity在task存在,拿到最顶端,不会启动新的Activity
intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);
//如果activity在task存在,将Activity之上的所有Activity结束掉
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
//默认的跳转类型,将Activity放到一个新的Task中
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//如果Activity已经运行到了Task,再次跳转不会在运行这个Activity
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
2 Spinner
- 获取默认值:Spinner.setSelection(int positon, boole anitme);
在做二级联动时此方法属于同步方法,需要开启异步任务。
Handler handler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
Spinner.setSelection(0, true);
}
});
或者做一个延时操作。
3 ScrollView
- 滚动:
Handler handler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
// ScrollView.FOCUS_DOWN 底部
// ScrollView.FOCUS_UP 顶部
mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
});
4 Android Studio支持java8
第一步 配置JAVA8
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
第二步 使用jack链
defaultConfig {
jackOptions {
enabled true
}
}
5 软盘
保持状态栏不被挤掉
android:windowSoftInputMode="stateVisible|adjustResize|stateHidden"
适合顶部有输入框
android:windowSoftInputMode="adjustPan|stateHidden"
6 屏幕旋转
在需要旋转的界面Activity注册清单中配置
android:configChanges="keyboardHidden|orientation|screenSize"
再在Activity中重写
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
// do something
} else {
// do something
}
}
7 ScrollView嵌套列表解决焦点停留在列表上的问题
在Scrollview第一层子View上设置属性
android:descendantFocusability="blocksDescendants"
8 View超出容器
第一步设置View的margin值为负数
android:layout_margint="-10dp"
第二步给父容器设置属性 false不裁剪
android:clipChildren="false"
9 防止页面打开时弹出软盘
添加属性
android:focusable="true"
android:focusableInTouchMode="true"
10 Formatter.formatFileSize(Context context, long sizeBytes)
格式化文件大小,将字节数据格式化为 B、KB、M 等单位的相应数据。context
参数用于判断返回结果的字符串顺序,right-to-left
还是 left-to-right
形式的。这个工具类免去我们自己转化计算的过程,非常方便,特别适用于应用内文件下载的类似场景。
11 TypedValue.applyDimension(int unit, float value, DisplayMetrics metrics)
- COMPLEX_UNIT_PX
- COMPLEX_UNIT_DIP
- COMPLEX_UNIT_PT
- COMPLEX_UNIT_SP
将指定单位的尺寸数据按照当前设备屏幕信息转化为相应的像素值。其中,TypedValue
为第一个参数提供了常用的单位值
12 Space
Space 是一个用于创建视图之间空隙的轻量级 View。在onDraw()
方法中不执行任何绘制,所以android:background
属性对他来说不起作用。通常我们使用 View 创建视图间的空隙,在不考虑背景色的情况下,Space
其实效率更高。注意,由于是 API 14 引入的控件,如果需要向前兼容的话,需要使用到 support v4 包。
13 view.performClick()
自动调用View
点击事件。通常按钮等控件只有在用户点击时才能触发其点击事件,该方法可以由某些特殊条件触发模拟用户点击行为。类似的还有performLongClick()
方法。
14 Log.getStackTraceString(Throwable tr)
Log
类提供的一个公共静态方法,与常见的Log.i()
等方法打印日志到logcat
控制台不同的是,该方法从Throwable
对象中获取错误信息,并以字符串的形式返回。当你需要做错误信息的数据持久化,比如保存至本地存储卡中或者上传至服务器时,利用这个方法就非常方便。
15 Linkify.addLinks()
我们知道对于TextView
文本控件中的内容,通过android:autoLink
属性可以为其添加诸如web
、phone
等固定模版的超链接点击事件。但毕竟系统模版有限,而利用Linkify.addLinks()
方法可以添加一些应用内自定义模版,比如新浪微博中的 "@XXX"
格式的超链接跳转等,都可以通过自定义正则表达式来匹配处理。
16 禁止截屏
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE)
设置安全窗口,禁用系统截屏。防止App中的一些界面被截屏,并显示在其他设备中造成信息泄漏。
比如支付宝App的“向商家付款”的包含付款二维码的界面。(补充说明一点,微信付款界面不是这么做的,采用的是在onResume()
生命周期方法中实时刷新付款二维码,与支付宝在安全方法采取的手段不同。)
17 拦截Bac 键,使App进入后台而不是关闭
@Override
public void onBackPressed() {
Intent launcherIntent = new Intent(Intent.ACTION_MAIN);
launcherIntent.addCategory(Intent.CATEGORY_HOME);
startActivity(launcherIntent);
}
使用Back键返回桌面,但不关闭当前应用,而是使之进入后台,就像按下Home键一样。
这个技巧厉害了。通常为了防止出现用户误按Back键退出App的情况,我们会在应用首页的Activity
中监听返回键操作,使用Toast
弱提示甚至Dialog
强提示的方式给到用户一个再次确认的操作,但无法阻止用户通过返回键逐步关闭应用。
然而,如果用这个方法拦截 App 最后一个 Activity
(常见为首页界面),既没有阻碍用户操作(回到桌面),又没有关闭掉我们的应用(后台运行中),间接提高 App 的存活时间,真乃暗度陈仓。并且据我实验,微信、支付宝、微博等 App 都是这么做的,大家不妨一试。
另外其他几种方法
- finish(); 正常退出遵循android系统规则
- android.os.Process.killProcess(android.os.Process.myPid());
- System.exit(0); java退出机制
- moveTaskToBack(true); 退出程序后台保活
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
if ((System.currentTimeMillis() - exitTime) > 2000) {
ToastUtil.getInstace(getActivity()).show("再按一次退出程序");
exitTime = System.currentTimeMillis();
} else {
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
moveTaskToBack(true);
}
}
return true;
}
return super.onKeyDown(keyCode, event);
}
18 缩略图工具类ThumbnailUtils
缩略图工具类,可以根据本地视频文件源、Bitmap 对象生成缩略图,常用的公共静态方法为:
- createVideoThumbnail(String filePath, int kind)
- extractThumbnail(Bitmap source, int width, int height)
19 bitmap.extractAlpha()
从源bitmap
中根据alpha
获取一个新的bitmap
对象。比较绕口,通常App中的Icon多数是纯色透明像素背景组成,利用这个方法可以对该图的非透明区域着色,有多种使用场景,常见如Button的pressed
状态,View 的阴影状态等。举个例子:
private static Bitmap getDropShadow(ImageView iv, Bitmap src, float radius, int color) {
final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
final int width = src.getWidth(), height = src.getHeight();
final Bitmap dest = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(dest);
final Bitmap alpha = src.extractAlpha();
canvas.drawBitmap(alpha, 0, 0, paint);
final BlurMaskFilter filter = new BlurMaskFilter(radius, BlurMaskFilter.Blur.OUTER);
paint.setMaskFilter(filter);
canvas.drawBitmap(alpha, 0, 0, paint);
iv.setImageBitmap(dest);
return dest;
}
20 ArgbEvaluator
系统提供的一个TypeEvaluator
,我们只需要提供两个起始颜色值和一个分值,系统会通过特定的算法计算得出一个新的颜色中间值。利用这个类,我们至少可以做两件事情。
第一,用于属性动画中。由于其实现了TypeEvaluator
接口,可以用来做自定义属性动画的求值器,改变 View 的显示状态。比如:
int colorStart = ContextCompat.getColor(this, R.color.black);
int colorEnd = ContextCompat.getColor(this, R.color.green);
ValueAnimator valueAnimator = ValueAnimator
.ofObject(new ArgbEvaluator(), colorStart, colorEnd)
.setDuration(3000);
valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
textView.setTextColor((Integer) animation.getAnimatedValue());
}
});
valueAnimator.start();
第二,利用该类提供的颜色求值算法,配合ViewPager
提供的滑动偏离值使用。这种场景常见于使用ViewPager
实现的引导页,其背景色随着滑动距离动态改变;使用ViewPager
实现的Tab样式菜单页面,Tab 中文本内容随着滑动距离动态改变字体颜色(可以参考安卓版微信)。这两种使用都使得ViewPager
页面切换过渡得很自然,体验极佳。如:
viewPager.addOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
new ArgbEvaluator().evaluate(positionOffset, startColor, endColor);
}
@Override
public void onPageSelected(int position) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
另外,关于颜色差值的计算,Google Sample里有另一种算法,可参考SlidingTabStrip.java
文件源码,核心方法内容如下:
/**
* Blend {@code color1} and {@code color2} using the given ratio.
*
* @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend,
* 0.0 will return {@code color2}.
*/
private static int blendColors(int color1, int color2, float ratio) {
final float inverseRation = 1f - ratio;
float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation);
float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation);
float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation);
return Color.rgb((int) r, (int) g, (int) b);
}
21 android:weightSum
用于LinearLayout
中,用于设置Childrenweight
的总比重。在LinearLayout
的children中,我们经常会使用android:layout_weight
按比例分配容器布局的空间,但有时候不一定会分完。以往,有些朋友可能会使用一个空 放在最后来达到末尾占位效果。如果你知道这个属性的话,就能少写一些代码。
22 android:descendantFocusability
用于ViewGroup
中,解决作为Parent
的ViewGroup
与Children View
之间的焦点占用问题。最最常见的使用场景就是 list item 中含有一些点击效果的控件,比如 Button
、CheckBox
等,相信大家都遇到过。取值有三种,含义就不用再多说了:
- afterDescendants viewgroup只有当其子类控件不需要获取焦点时才获取焦点
- beforeDescendants viewgroup会优先其子类控件而获取到焦点
- blocksDescendants viewgroup会覆盖子类控件而直接获得焦点
23 android:duplicateParentState
是否将View
自身的drawable state
交给直接parent ViewGroup
控制,值为boolean
类型。比如有一个item
布局, item
中有一个button
,如果点击item layout
时,需要button
呈现对应的点击效果,就可以在button
中用到这个属性。不过,从设计的角度来讲,这种场景还是比较少见的。知道有这个属性就好,不推荐这种交互设计。
24 android:fillViewport
ScrollView
的一个属性,用于设置内容部分是否填满屏幕,主要针对内容不足以填满屏幕的情况。
25 android:adjustViewBounds
使用ImageView
时,你可能会用android:scaleType
属性设置图片缩放方式。殊不知,android:adjustViewBounds
属性也能起到类似的效果。但要注意的是,后者需要至少指定ImageView
宽高中的一个属性,或者maxHeight
之类的,然后另一个属性随之适配。这个属性用在列表中较为合适,比如App中的活动列表页面,图片宽度设置为match_parent
,然后高度设为wrap_content
使其自适应,这样便能保证从服务获取的高分辨率图片在不同的屏幕中不被拉伸变形。(备注:最好在项目资源文件中放置一个与网络图片相同尺寸的默认图,起到placeholder
作用,避免图片显示前高度为 0 的较差体验。)
26 Android动画——布局动画、转场动画
布局动画的作用于ViewGroup
,执行动画效果的是内部的子View
。布局动画在android中可以通过LayoutAnimation
或LayoutTransition
来实现。
- LayoutAnimation
LayoutAnimation
实际上是一个View
动画,用来控制子View显示时的动画效果。可以通过Java代码或者Xml文件来定义LayoutAnimation
动画。
使用java代码:
private void setLayoutAnimation() {
Animation animation = AnimationUtils.loadAnimation(this, R.anim.layout_item_anim_set);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
mListView.setLayoutAnimation(controller);
}
使用xml:
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation
xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/layout_item_anim_set"
android:animationOrder="normal"
android:delay="0.5">
</layoutAnimation>
在ViewGroup
所在布局中调用:
android:layoutAnimation="@anim/layout_anim"
- LayoutTransition
LayoutTransition
用于在ViewGroup
中有子View添加、删除、隐藏、显示时所有子View动画效果。LayoutTransition
有5中动画变化形式
- LayoutTransition.APPEARING:子View添加到容器中时的动画效果
- LayoutTransition.CHANGE_APPEARING:子View添加到容器中时,其他子View位置改变的动画效果
- LayoutTransition.DISAPPEARING:子View被移除时的动画效果
- LayoutTransition.CHANGE_DISAPPEARING:子View被移除时,其他子View的动画效果
- LayoutTransition.CHANGING:子View在容器中位置变化时其他子View的动画效果
使用默认的动画样式:
只需要在使用的LinearLayout
、FrameLayout
、RelativeLayout
等ViewGroup容器的布局文件中添加android:animateLayoutChanges="true"
即可,系统会使用默认的LayoutTransition
来实现子View添加、删除或变化是的动画效果。
27 Handler
- 定义
Handler
允许您发送和处理Message
和Runnable
与线程的MessageQueue
关联的对象。每个Handler
实例与单个线程和该线程的MessageQueue
相关联。当你创建一个新的Handler
时,它绑定到线程/正在创建它的线程的消息队列 - 从那时起,它会将消息和可运行文件传递到该消息队列并执行他们从消息队列中出来。 - 处理程序有两个主要用途
(1)调度Message
和Runnable
将在未来的某个时候执行;
(2)入队在不同于你自己的线程上执行的动作。
private static class MessageHandler extends Handler {
WeakReference<ImageLoadActivity> reference;
MessageHandler(ImageLoadActivity activity) {
reference = new WeakReference<>(activity);
}
// 处理消息
@Override
public void handleMessage(android.os.Message msg) {
final ImageLoadActivity activity = reference.get();
switch (msg.what) {
case 0:
Toast.makeText(activity, R.string.clear, Toast.LENGTH_LONG).show();
break;
}
super.handleMessage(msg);
}
}
// 创建
MessageHandler handler = new MessageHandler(this);
// 发送消息
new Thread(() -> {
Glide.get(this).clearDiskCache(); // 清理磁盘缓存
handler.sendEmptyMessage(0);
}).start();
28 Looper
- 定义
用于为线程运行消息循环的类。 线程默认情况下没有与他们相关联的消息循环; 创建一个,调用Looper.prepare()
在线程中运行循环,然后调用Looper.loop()
让它处理消息,直到循环停止。大多数与消息循环的交互是通过的Handler
类。
class LooperThread extends Thread {
Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}
- Looper主要作用:
1、与当前线程绑定,保证一个线程只会有一个Looper实例,同时一个Looper实例也只有一个MessageQueue。
2、loop()方法,不断从MessageQueue中去取消息,交给消息的target属性的dispatchMessage去处理。
好了,我们的异步消息处理线程已经有了消息队列(MessageQueue),也有了在无限循环体中取出消息的哥们,现在缺的就是发送消息的对象了,于是乎:Handler登场了。
29 Message
定义
定义一个包含可以是描述和任意数据对象的消息发送到Handler
。 这个对象包含两个额外的int
字段和一个
额外的object
字段允许您在许多情况下不进行分配。虽然Message
的构造函数是公开的,但最好的方式是获取其中之一是调用Message.obtain()
或其中一个Handler.obtainMessage()
方法,这将拉他们来自一个循环再造的物体池。问题
2.1 一个线程是否只有一个Looper?
2.2 如何保证一个线程只有一个Looper?
30 android子线程更新UI
android提供以下几种方法在主线程更新ui
- Activity.runOnUiThread(Runnable)
- View.post(Runnable)
- View.postDelayed(Runnable, long)
- Handler
31 关于Handler,Looper,Message总结
- 首先Looper.prepare()在本线程中保存一个Looper实例,然后该实例中保存一个MessageQueue对象;因为Looper.prepare()在一个线程中只能调用一次,所以MessageQueue在一个线程中只会存在一个。
- Looper.loop()会让当前线程进入一个无限循环,不端从MessageQueue的实例中读取消息,然后回调msg.target.dispatchMessage(msg)方法。
- Handler的构造方法,会首先得到当前线程中保存的Looper实例,进而与Looper实例中的MessageQueue想关联。
- Handler的sendMessage方法,会给msg的target赋值为handler自身,然后加入MessageQueue中。
- 在构造Handler实例时,我们会重写handleMessage方法,也就是msg.target.dispatchMessage(msg)最终调用的方法。
- 好了,总结完成,大家可能还会问,那么在Activity中,我们并没有显示的调用Looper.prepare()和Looper.loop()方法,为啥Handler可以成功创建呢,这是因为在Activity的启动代码中,已经在当前UI线程调用了Looper.prepare()和Looper.loop()方法