Skip to content

Commit 3b365ca

Browse files
author
shhu
committed
添加图片分享
1 parent dad30f6 commit 3b365ca

File tree

3 files changed

+111
-58
lines changed

3 files changed

+111
-58
lines changed

README.md

Lines changed: 74 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
* `Android 6.0` 以前,应用要想保存图片到相册,只需要通过`File`对象打开IO流就可以保存;
99
* `Android 6.0` 添加了运行时权限,需要先申请存储权限才可以保存图片;
10-
* `Android 10` 引入了分区存储,但不是强制的,可以通过`requestLegacyExternalStorage=true`关闭分区存储;
10+
* `Android 10` 引入了分区存储,但不是强制的,可以通过清单配置`android:requestLegacyExternalStorage="true"`关闭分区存储;
1111
* `Android 11` 强制开启分区存储,应用以 Android 11 为目标版本,系统会忽略 `requestLegacyExternalStorage`标记,访问共享存储空间都需要使用`MediaStore`进行访问。
1212

1313
我们通过上面的时间线可以看出,Google对系统公共存储的访问的门槛逐渐升高,摒弃传统的Java File对象直接访问文件的方式,想将Android的共享空间访问方式统一成一套API。这是我们的主角`MediaStore`
@@ -24,7 +24,7 @@
2424
1. 先将图片记录插入媒体库,获得插入的Uri;
2525
2. 然后通过插入Uri打开输出流将文件写入;
2626

27-
大致流程就是这样子,只是不同的版本有一些细微的差距
27+
大致流程就是这样子,只是不同的系统版本有一些细微的差距
2828

2929
* Android 10 之前的版本需要申请存储权限,**Android 10及以后版本是不需要读写权限的**
3030
* Android 10 之前是通过File路径打开流的,所以需要判断文件是否已经存在,否者的话会将以存在的图片给覆盖
@@ -34,29 +34,31 @@
3434

3535
## 编码时间
3636

37-
这里用保存Bitmap到图库为例,保存文件和权限申请的逻辑,这里就不贴代码了,详见[Demo](https://github.com/hushenghao/MediaStoreDemo.git)
38-
39-
```kotlin
40-
// 为了演示方便,生产环境记得在IO线程处理
41-
// decode bitmap
42-
val bitmap = BitmapFactory.decodeStream(assets.open("wallhaven_rdyyjm.jpg"))
43-
// 保存bitmap到相册
44-
val uri = bitmap.saveToAlbum(context, fileName = "save_wallhaven_rdyyjm.jpg")
37+
这里用保存Bitmap到图库为例,保存文件 和 权限申请的逻辑,这里就不贴代码了,详见 [Demo](https://github.com/hushenghao/MediaStoreDemo.git)
38+
39+
检查清单文件,如果应用里没有其他需要存储权限的需求可以加上`android:maxSdkVersion="28"`,这样Android 10的设备的应用详情就看不到这个权限了。
40+
```xml
41+
<!--Android Q之后不需要存储权限,完全使用MediaStore API来实现-->
42+
<uses-permission
43+
android:name="android.permission.READ_EXTERNAL_STORAGE"
44+
android:maxSdkVersion="28" />
45+
<uses-permission
46+
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
47+
android:maxSdkVersion="28" />
4548
```
46-
47-
是的很简单,详细实现是怎么弄的,接着往下看。
48-
49+
保存图片到相册。这里为了演示方便,生产环境记得在IO线程处理,ANR了可不怪我。
4950
```kotlin
50-
const val MIME_PNG = "image/png"
51-
const val MIME_JPG = "image/jpg"
52-
// 保存位置,这里使用Picures,也可以改为 DCIM
53-
private val ALBUM_DIR = Environment.DIRECTORY_PICTURES
51+
private fun saveImageInternal() {
52+
val uri = assets.open("wallhaven_rdyyjm.jpg").use {
53+
it.saveToAlbum(this, fileName = "save_wallhaven_rdyyjm.jpg", null)
54+
} ?: return
5455

55-
/**
56-
* 用于Q以下系统获取图片文件大小来更新[MediaStore.Images.Media.SIZE]
57-
*/
58-
private class OutputFileTaker(var file: File? = null)
56+
Toast.makeText(this, uri.toString(), Toast.LENGTH_SHORT).show()
57+
}
58+
```
5959

60+
是不是很简单,详细实现是怎么弄的,接着往下看。这是一个保存Bitmap的扩展方法
61+
```kotlin
6062
/**
6163
* 保存Bitmap到相册的Pictures文件夹
6264
*
@@ -91,38 +93,19 @@ fun Bitmap.saveToAlbum(
9193
}
9294
return imageUri
9395
}
96+
```
9497

95-
private fun Uri.outputStream(resolver: ContentResolver): OutputStream? {
96-
return try {
97-
// 通过Uri打开输出流。同理也可以打开输入流,读取媒体库文件
98-
resolver.openOutputStream(this)
99-
} catch (e: FileNotFoundException) {
100-
Log.e(TAG, "save: open stream error: $e")
101-
null
102-
}
103-
}
98+
插入图片到媒体库,需要注意Android 10以下需要图片查重,防止文件被覆盖的问题。
99+
```kotlin
100+
const val MIME_PNG = "image/png"
101+
const val MIME_JPG = "image/jpg"
102+
// 保存位置,这里使用Picures,也可以改为 DCIM
103+
private val ALBUM_DIR = Environment.DIRECTORY_PICTURES
104104

105-
private fun Uri.finishPending(
106-
context: Context,
107-
resolver: ContentResolver,
108-
outputFile: File?
109-
) {
110-
val imageValues = ContentValues()
111-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
112-
if (outputFile != null) {
113-
// Android 10 以下需要更新文件大小字段,否则部分设备的图库里照片大小显示为0
114-
imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length())
115-
}
116-
resolver.update(this, imageValues, null, null)
117-
// 通知媒体库更新,部分设备不更新 图库看不到 ???
118-
val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this)
119-
context.sendBroadcast(intent)
120-
} else {
121-
// Android Q添加了IS_PENDING状态,为0时其他应用才可见
122-
imageValues.put(MediaStore.Images.Media.IS_PENDING, 0)
123-
resolver.update(this, imageValues, null, null)
124-
}
125-
}
105+
/**
106+
* 用于Q以下系统获取图片文件大小来更新[MediaStore.Images.Media.SIZE]
107+
*/
108+
private class OutputFileTaker(var file: File? = null)
126109

127110
/**
128111
* 插入图片到媒体库
@@ -226,14 +209,36 @@ private fun ContentResolver.queryMediaImage28(imagePath: String): Uri? {
226209
}
227210
return null
228211
}
229-
230-
private const val TAG = "ImageExt"// Log tag
231212
```
232-
**大家期盼已久的代码** [ImageExt.kt](https://github.com/hushenghao/MediaStoreDemo)
213+
改变标志位,通知媒体库我完事了,到这里整个图片保存就结束了。怎么样是不是很简单,赶紧去系统图库里看看图片是不是已经在了。
214+
```kotlin
215+
private fun Uri.finishPending(
216+
context: Context,
217+
resolver: ContentResolver,
218+
outputFile: File?
219+
) {
220+
val imageValues = ContentValues()
221+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
222+
if (outputFile != null) {
223+
// Android 10 以下需要更新文件大小字段,否则部分设备的图库里照片大小显示为0
224+
imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length())
225+
}
226+
resolver.update(this, imageValues, null, null)
227+
// 通知媒体库更新,部分设备不更新 图库看不到 ???
228+
val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this)
229+
context.sendBroadcast(intent)
230+
} else {
231+
// Android Q添加了IS_PENDING状态,为0时其他应用才可见
232+
imageValues.put(MediaStore.Images.Media.IS_PENDING, 0)
233+
resolver.update(this, imageValues, null, null)
234+
}
235+
}
236+
```
237+
虽然代码有点多,但是相信**大家期盼已久了** [ImageExt.kt](https://raw.githubusercontent.com/hushenghao/MediaStoreDemo/main/app/src/main/java/com/dede/mediastoredemo/ImageExt.kt)
233238

234239
## 图片分享
235240

236-
有很多场景是保存图片之后,调用第三方分享进行图片分享,但是一些文章不管三七二十一说需要用`FileProvider`。实际上这是不准确的,大部分情况是需要,一些场景是不需要的
241+
有很多场景是保存图片之后,调用第三方分享进行图片分享,但是一些文章不管三七二十一说需要用`FileProvider`。实际上这是不准确的,部分情况是需要,还有一些场景是不需要的
237242

238243
我们只需要记得 **FileProvider是给其他应用分享应用私有文件的** 就够了,只有在我们需要将应用沙盒内的文件共享出去的时候才需要配置FileProvider。例如:
239244

@@ -242,12 +247,24 @@ private const val TAG = "ImageExt"// Log tag
242247

243248
但是保存到系统图库并分享的场景明显就不符合这个场景,因为图库不是应用私有的空间。
244249

250+
```
251+
private fun shareImageInternal() {
252+
val uri = assets.open("wallhaven_rdyyjm.jpg").use {
253+
it.saveToAlbum(this, fileName = "save_wallhaven_rdyyjm.jpg", null)
254+
} ?: return
255+
val intent = Intent(Intent.ACTION_SEND)
256+
.putExtra(Intent.EXTRA_STREAM, uri)
257+
.setType("image/*")
258+
startActivity(Intent.createChooser(intent, null))
259+
}
260+
```
261+
245262
所以在使用FileProvider要区分一下场景,是不是可以不需要,因为FileProvider是一种特殊的ContentProvider,每一个内容提供者在应用启动的时候都要初始化,所以也会拖慢应用的启动速度。
246263

247264
## 参考资料
248-
265+
[Demo](https://github.com/hushenghao/MediaStoreDemo.git)
249266
[访问共享存储空间中的媒体文件](https://developer.android.google.cn/training/data-storage/shared/media)
250-
[MediaStore](https://developer.android.google.cn/reference/android/provider/MediaStore)
267+
[Android MediaStore](https://developer.android.google.cn/reference/android/provider/MediaStore)
251268
[OpenSDK支持FileProvider方式分享文件到微信](
252269
https://developers.weixin.qq.com/community/develop/doc/0004886026c1a8402d2a040ee5b401)
253270

app/src/main/java/com/dede/mediastoredemo/MainActivity.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.dede.mediastoredemo
22

33
import android.Manifest
4+
import android.content.Intent
45
import android.graphics.BitmapFactory
56
import android.os.Build
67
import android.os.Bundle
@@ -35,7 +36,7 @@ class MainActivity : AppCompatActivity() {
3536
Toast.makeText(this, uri.toString(), Toast.LENGTH_SHORT).show()
3637
}
3738

38-
fun saveImage(v: View) {
39+
fun saveImage(view: View) {
3940
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
4041
saveImageInternal()
4142
} else {
@@ -48,4 +49,28 @@ class MainActivity : AppCompatActivity() {
4849
}
4950
}
5051
}
52+
53+
private fun shareImageInternal() {
54+
val uri = assets.open("wallhaven_rdyyjm.jpg").use {
55+
it.saveToAlbum(this, fileName = "save_wallhaven_rdyyjm.jpg", null)
56+
} ?: return
57+
val intent = Intent(Intent.ACTION_SEND)
58+
.putExtra(Intent.EXTRA_STREAM, uri)
59+
.setType("image/*")
60+
startActivity(Intent.createChooser(intent, null))
61+
}
62+
63+
fun shareImage(view: View) {
64+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
65+
shareImageInternal()
66+
} else {
67+
val permissions = arrayOf(
68+
Manifest.permission.READ_EXTERNAL_STORAGE,
69+
Manifest.permission.WRITE_EXTERNAL_STORAGE
70+
)
71+
launcherCompat.launch(permissions) {
72+
shareImageInternal()
73+
}
74+
}
75+
}
5176
}

app/src/main/res/layout/activity_main.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
app:layout_constraintTop_toTopOf="parent" />
1515

1616
<Button
17+
android:id="@+id/bt_save"
1718
android:layout_width="wrap_content"
1819
android:layout_height="wrap_content"
1920
android:onClick="saveImage"
@@ -24,4 +25,14 @@
2425
app:layout_constraintTop_toTopOf="parent"
2526
tools:ignore="HardcodedText,UsingOnClickInXml" />
2627

28+
<Button
29+
android:layout_width="wrap_content"
30+
android:layout_height="wrap_content"
31+
android:onClick="shareImage"
32+
android:text="Share Image"
33+
app:layout_constraintLeft_toLeftOf="parent"
34+
app:layout_constraintRight_toRightOf="parent"
35+
app:layout_constraintTop_toBottomOf="@id/bt_save"
36+
tools:ignore="HardcodedText,UsingOnClickInXml" />
37+
2738
</androidx.constraintlayout.widget.ConstraintLayout>

0 commit comments

Comments
 (0)