Android 图像

CameraAlbum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
* 第一行代码第三版
*/
class CameraAlbumActivity : BaseActivity() {

/**
* 调用摄像头拍照
*/
val takePhoto = 1
lateinit var imageUri: Uri
lateinit var outputImage: File

/**
* 调用相册
*/
val fromAlbum = 2

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cemara)

// 在 NotificationActivity 测试通知的点击事件,会跳转到此页面,以下代码是当点击事件完成后取消通知的一种方式。
// 显示地调用 NotificationManager 的 cancel() 取消通知。
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// 点击这个通知时,通知会自动取消。否则通知消息会一直留存在系统状态栏上。
// 这个 1 是指在创建通知时给每条通知指定的 id,想取消哪条通知,就传入该通知的 id 即可。
manager.cancel(1)

initData()
}

private fun initData() {
takePhoto()
fromAlbum()
}

/**
* 调用相册
*/
private fun fromAlbum(){
btnFromAlbum.setOnClickListener{
// 打开系统的文件选择器
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
// 指定只显示图片,过滤条件
intent.type = "image/*"
startActivityForResult(intent,fromAlbum)
}
}

/**
* 调用摄像头拍照
*/
private fun takePhoto() {
btnTakePhoto.setOnClickListener{
// 创建 File 对象,用于存储拍照后的照片,存放在手机 SD 卡的应用关联缓存目录下。
// 应用关联目录:指 SD 卡中专门用于存放当前应用缓存数据的位置,
// 调用 getExternalCacheDir() 可以得到这个目录,具体路径 /sdcard/Android/data/<package name>/cache。
// 从 Android 6.0 开始,读写 SD 卡被列为危险权限,如果将图片存放在 SD 卡的任何其它目录,都要运行时权限处理,而使用应用关联目录则可以跳过这一步。
// 另外,从 Android 10.0 开始,公有的 SD 卡目录已经不再允许被应用程序直接访问了,而是要使用作用域存储才行。
outputImage = File(externalCacheDir,"output_image.jpg")
if (outputImage.exists()){
outputImage.delete()
}
outputImage.createNewFile()
// 从 Android 7.0 系统开始,直接使用本地真实路径的 Uri 被认为是不安全的,会抛出一个 FileUriExposedException 异常。
// 而 FileProvider 则是一种特殊的 ContentProvider,它使用了和 ContentProvider 类似的机制来对数据进行保护,
// 可以选择性地将封装过的 Uri 共享给外部,从而提高了应用的安全性。
imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
// 调用 FileProvider.getUriForFile() 将 File 对象转换成一个封装过的 Uri 对象。
// 第二个参数可以是任意唯一的字符串,第三个参数是刚刚创建的 File 对象。
// FileProvider 要记得对它在 AndroidManifest.xml 文件内进行注册才行。
FileProvider.getUriForFile(this,"com.example.myapplication.fileprovider",outputImage)
}else {
// 如果运行设备系统版本低于 Android 7.0,就调用 Uri 的 fromFile() 将 File 对象转换成 Uri 对象,
// 这个 Uri 对象标识着 output_image.jpg 的本地真实路径。
Uri.fromFile(outputImage)
}
// 启动相机程序
val intent = Intent("android.media.action.IMAGE_CAPTURE")
intent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri) // 指定图片的输出地址,填入刚得到的 Uri 对象。
startActivityForResult(intent,takePhoto) // 一个隐式启动,照相机程序会被匹配打开,拍下的照片将会输出到 output_image.jpg 中。
}
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when(requestCode){
takePhoto -> {
if (resultCode == Activity.RESULT_OK){
// 调用 BitmapFactory 的 decodeStream() 将 output_image.jpg 这张照片解析成 Bitmap 对象
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))
// 将拍摄的照片显示出来
ivPhoto.setImageBitmap(rotateIfRequired(bitmap))
}
}
fromAlbum -> {
if (resultCode == Activity.RESULT_OK && data != null){
data.data?.let { uri ->
// 将选择的图片显示
val bitmap = getBitmapFromUri(uri)
ivPhoto.setImageBitmap(bitmap)
}
}
}
}
}

private fun getBitmapFromUri(uri: Uri) = contentResolver.openFileDescriptor(uri,"r")?.use {
BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
}

/**
* 照片旋转
* 因为一些手机认为打开摄像头进行拍摄时手机就应该是横屏的,因此回到竖屏的情况下就会发生 90 度的旋转。
*/
private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
val exif = ExifInterface(outputImage.path)
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_NORMAL)
return when(orientation){
ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap,90)
ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap,180)
ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap,270)
else -> bitmap
}
}

private fun rotateBitmap(bitmap: Bitmap, degree: Int): Bitmap {
val matrix = Matrix()
matrix.postRotate(degree.toFloat())
val rotatedBitmap = Bitmap.createBitmap(bitmap,0,0,bitmap.width,bitmap.height,matrix,true)
bitmap.recycle() // 将不再需要的 Bitmap 对象回收
return rotatedBitmap
}

companion object{
fun actionStart(context: Context){
val intent = Intent(context, CameraAlbumActivity::class.java)
context.startActivity(intent)
}
}
}
AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
<!-- name 的属性是固定的,authorities 的值必须一致。 -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.myapplication.fileprovider"
android:exported="false"
android:grantUriPermissions="true">

<!-- meta-data 指定 Uri 的共享路径,并引用了一个 @xml/file_paths 资源。 -->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

通过三种方式绘制图片

在 Android 平台绘制一张图片,使用至少 3 种不同的 API,ImageView,SurfaceView,自定义 View。

ImageView 绘制图片

1
2
3
// val bitmap = BitmapFactory.decodeResource(resources,R.drawable.ic_launcher_background)  // 返回 null,犯傻了,加载了个 xml。
val bitmap = BitmapFactory.decodeResource(resources,R.drawable.img)
iv.setImageBitmap(bitmap)

SurfaceView 绘制图片

什么是 SurfaceView

首先 SurfaceView 本身也是一个 View。SurfaceView 是视图(View)的继承类,这个视图里内嵌了一个专门用于绘制的 Surface。可以控制这个 Surface 的格式和尺寸。SurfaceView 控制这个 Surface 的绘制位置。一般来说,能看到的一个窗口就是一个Surface,大概可以理解为一个 Activity 创建出一个可视化界面,而这个界面就可以理解为一个 Surface,Activity 在这个 Surface 上不断绘制,从而看到 Android 上的界面。Surface 是纵深排序(Z-ordered)的,它总在自己所在窗口的后面 SurfaceView 提供了一个可见区域,只有在这个可见区域内 的 Surface部分内容才可见,可见区域外的部分不可见。Surface 的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者 Surface 的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件)。

为什么用 SurfaceView

SurfaceView 的核心在于提供了两个线程:UI线程和渲染线程。如果用 View 本身来绘制,效率不高。

SurfaceView 的 UI 就可以在一个独立的线程中进行绘制,可以不会占用主线程资源,还有就是 SurfaceView 使用了双缓冲机制,双缓冲机制的原理就是可以看成两个 Canvas,一个用于前台显示,另外一个用于储存前台上一次显示的画布,所操作的都是在第二个 Canvas上面进行,进行完了之后再替换为新的 Canvas,这样重复的交替绘制来渲染,使整个画面会更加的流畅。

总的来讲

  1. 它拥有独立的特殊的绘制表面,它不与其宿主窗口共享一个绘制表面。
  2. SurefaceView 的 UI 可以在一个独立的线程中进行绘制。
  3. 因为不会占用主线程资源,一是可以实现复杂而高效的UI,二是不会导致用户输入得不到及时响应。

综合这些特点,SurfaceView 一般用来实现动态的或者比较复杂的图像还有动画的显示。

3、如何使用 SurfaceView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 为 SurfaceHolder 对象设置回调监听
sv.holder.addCallback(object : SurfaceHolder.Callback{
/**
* 作用于 Surface 在任何结构(格式或者尺寸)的变化,都会立马回调这个方法。就是在这个点去更新图像。
* 这个方法总是至少被调用一次。换句话说就是可能会被调用多次。
*/
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) { }

/**
* 当 Surface 即将销毁的时候,会立马回调此方法。
* 如果有一个正在执行渲染的线程。当这个方法返回之前,应该确保这个线程再也不会去接触这个 Surface 了。
*/
override fun surfaceDestroyed(holder: SurfaceHolder?) { }

/**
* 当 Surface 第一次被创建的时候会立马回调这个方法,可以在这里初始化一些所需要的东西渲染代码。
* 而需要注意的是只有一条线程可以在 Surface 上绘制,想渲染绘制应该是在其他的线程。正常渲染将在另一个线程中。
*/
override fun surfaceCreated(holder: SurfaceHolder?) {
if (holder == null){
return
}
val paint = Paint() // 初始化画笔
paint.setAntiAlias(true) // 抗锯齿
paint.setStyle(Paint.Style.STROKE)

val bitmap = BitmapFactory.decodeResource(resources,R.drawable.img) // 获取bitmap
val canvas = holder.lockCanvas() // 先锁定当前surfaceView的画布
canvas.drawBitmap(bitmap, Matrix(), paint)
holder.unlockCanvasAndPost(canvas) // 解除锁定并显示在界面上
}
})

自定义 View 绘制图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class CustomView : View {

private lateinit var paint : Paint
private lateinit var bitmap : Bitmap

constructor(context: Context?) : super(context){
init()
}

constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs){
init()
}

private fun init() {
paint = Paint()
paint.isAntiAlias = true
paint.style = Paint.Style.STROKE

bitmap = BitmapFactory.decodeResource(resources,R.drawable.img)
}

override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)

canvas?.drawBitmap(bitmap, Matrix(), paint)
}
}
1
2
3
<com.example.testdemo.CustomView
android:layout_width="match_parent"
android:layout_height="200dp"/>

备注

参考

Android Camera2 教程