当前位置: 首页 > news >正文

一个Android下载网络图片显示并保存到系统相册的完整案例

文章目录

    • 1. 案例简介
      • 1.1 效果演示
    • 2. 工程配置
    • 3. 网络层
      • 3.1 网络接口定义
      • 3.2 Retrofit工具类
    • 4. 主界面及完整代码
      • 4.1 完整主界面代码 ImageDownloadActivity
      • 4.2 布局文件
    • 5. 总结

1. 案例简介

在 Android开发 中,下载图片保存到本地是常见的需求,看似简单但其中包含了一些比较关键的知识点,比如网络请求、文件下载、动态权限申请、文件保存、移动到系统相册

1.1 效果演示

下载网络图片

2. 工程配置

本案例使用 Android Studio 开发,kotlin语言。 build.gradle 文件中的必要依赖有:

// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'// RxJava
implementation 'io.reactivex.rxjava2:rxjava:2.2.20'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'// Retrofit RxJava Adapter
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'// 使用 Gson
implementation 'com.google.code.gson:gson:2.8.9'

3. 网络层

使用 Retrofit 网络请求框架+RxJava 异步数据流处理库。

3.1 网络接口定义

接口层定义了下载文件的方法 downloadFile,接收参数是下载地址的相对路径。因为 Retrofit 在构建的时候会有一个 BaseUrl,拼接上这里的相对路径就是完整的下载地址。

import io.reactivex.Single
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Urlinterface INetApi {@GETfun downloadFile(@Url relativePath: String?): Single<ResponseBody>
}

3.2 Retrofit工具类

一般都会创建一个 RetrofitUtil 工具类来创建 Retrofit 实例对象供外界调用。这里的BaseUrl 也由外界传入,可以创建多个不同的实例。

import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactoryobject RetrofitUtil {fun createRetrofit(baseUrl: String): INetApi {val retrofit = Retrofit.Builder().baseUrl(baseUrl).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).addConverterFactory(GsonConverterFactory.create()).build()return retrofit.create(INetApi::class.java)}
}

4. 主界面及完整代码

这里我们搭建一个简单的下载图片的UI界面。图片的地址是网上随便找的一个网络图片地址:https://i2.hdslb.com/bfs/archive/961b6650675f97b297bb5ec764ce89b314134544.jpg
你也可以随便找一个,替换成你的地址。

  1. 界面中有一个按钮 Button 和一个图片 ImageView,点击按钮,触发 startDownload 方法,通过上面封装好的 RetrofitUtil 工具类,传入 BaseUrl 和 图片的相对地址,进行网络请求。

  2. 使用RxJava 处理网络返回数据,在收到返回的数据流后,用 BitmapFactory.decodeStream 解析成一张
    Bitmap 图片,显示到控件 ImageView 上。至此完成网络图片的显示。

  3. 接下来是将这张图片保存到手机中。在 trySaveImage 方法中,首先进行了动态权限检查/申请 ,得到存储权限后,调用
    saveToFile 方法,从 ImageView 上获取图片,创建一个文件准备接收图片,接着使用bitmap.compress
    结合 FileOutputStream 将图片数据压缩到文件中,至此完成了图片保存到本地文件。

  4. 但是,你会发现在手机的系统相册中找不到这张图片。这是因为上面保存的路径是应用内的目录,我们需要将文件拷贝到系统相册,才能在相册中显示。

  5. 于是,调用 moveToAlbum
    方法,将文件移动到系统相册。这里按系统版本会有不同的操作方式,Android10以下稍微麻烦点。都是使用了Android四大组件之一
    ContentProvider 与系统进行内容通信。

  6. 移动完毕之后,不要忘记删除原文件,防止数据冗余。

4.1 完整主界面代码 ImageDownloadActivity

import android.Manifest
import android.content.ContentResolver
import android.content.ContentValues
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmapOrNull
import com.example.mytest.R
import com.example.mytest.util.FileUtil
import com.example.mytest.util.RetrofitUtil
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException/*** 下载图片保存到相册* URL:https://i2.hdslb.com/bfs/archive/961b6650675f97b297bb5ec764ce89b314134544.jpg*/
class ImageDownloadActivity : AppCompatActivity() {private lateinit var container: ViewGroupprivate lateinit var imageView1: ImageViewprivate val imageUrl = "bfs/archive/961b6650675f97b297bb5ec764ce89b314134544.jpg"private val baseImageUrl = "https://i2.hdslb.com/"private var disposable: Disposable? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_image_download)container = findViewById(R.id.container)imageView1 = findViewById(R.id.imageView1)}fun startDownload(view: View) {disposable = RetrofitUtil.createRetrofit(baseImageUrl).downloadFile(imageUrl).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe({val bitmap = BitmapFactory.decodeStream(it.byteStream())imageView1?.setImageBitmap(bitmap)trySaveImage()}, {Log.e(Companion.TAG, "startDownload: ${it.message}")})}private fun trySaveImage() {if (ContextCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),0)} else {saveToFile()}}override fun onRequestPermissionsResult(requestCode: Int,permissions: Array<out String>,grantResults: IntArray) {super.onRequestPermissionsResult(requestCode, permissions, grantResults)if (requestCode == 0) {if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {saveToFile()} else {Toast.makeText(this, "拒绝了存储权限!", Toast.LENGTH_SHORT).show()}}}private fun saveToFile() {val bitmap = imageView1.drawable.toBitmapOrNull() ?: returntry {val fileSaveDirectory = File(getExternalFilesDir(null), "MyImage")if (!fileSaveDirectory.exists()) {fileSaveDirectory.mkdirs()}val fileName = System.currentTimeMillis().toString() + ".jpeg"val saveFile = File(fileSaveDirectory, fileName)Log.i(TAG, "saveToFile: ${saveFile.absolutePath}")val outputStream = FileOutputStream(saveFile)bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)outputStream.flush()outputStream.close()Toast.makeText(this, "保存成功!", Toast.LENGTH_SHORT).show()moveToAlbum(saveFile)Toast.makeText(this, "移动到相册成功!", Toast.LENGTH_SHORT).show()} catch (e: Exception) {Log.e(TAG, "saveToFile: ${e.message}", e)Toast.makeText(this, "保存失败!", Toast.LENGTH_SHORT).show()}}private fun moveToAlbum(file: File): Boolean {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {val values = ContentValues()values.put(MediaStore.MediaColumns.DISPLAY_NAME, file.name)values.put(MediaStore.MediaColumns.MIME_TYPE, "image/星") // 这里是 image/*values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + File.separator + "MyTest")val contentResolver: ContentResolver = contentResolverval uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return falsetry {val outputStream = contentResolver.openOutputStream(uri)?: return falseval fileInputStream = FileInputStream(file)FileUtil.copyFileStream(fileInputStream, outputStream)fileInputStream.close()outputStream.close()file.delete()return true} catch (e: IOException) {e.printStackTrace()return false}} else {val cameraDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)if (!cameraDir.exists()) {cameraDir.mkdirs()}val sicilyDirPath = cameraDir.absolutePath + File.separator + "MyTest"val sicilyDir = File(sicilyDirPath)if (!sicilyDir.exists()) {sicilyDir.mkdirs()}val finalPath = sicilyDir.absolutePath + File.separator + file.nameval values = ContentValues()values.put(MediaStore.Images.Media.DISPLAY_NAME, file.name)values.put(MediaStore.Images.Media.DATA, finalPath)Log.i(TAG, "moveToAlbum: $finalPath")val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return falsetry {val outputStream = contentResolver.openOutputStream(uri)?: return falseval fileInputStream = FileInputStream(file)FileUtil.copyFileStream(fileInputStream, outputStream)fileInputStream.close()outputStream.close()val outputFile = File(finalPath)val contentValues = ContentValues()contentValues.put(MediaStore.Images.Media.SIZE, outputFile.length())contentResolver.update(uri, contentValues, null, null)// 通知媒体库更新val intent = Intent(@Suppress("DEPRECATION") Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri)sendBroadcast(intent)file.delete()return true} catch (e: IOException) {e.printStackTrace()return false}}}override fun onDestroy() {super.onDestroy()disposable?.dispose()}companion object {private const val TAG = "ImageDownloadActivity"}
}

4.2 布局文件

对应的布局文件:activity_image_download.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/container"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".imagedownload.ImageDownloadActivity"><ImageViewandroid:id="@+id/imageView1"android:layout_width="200dp"android:layout_height="200dp"android:scaleType="centerCrop"android:background="#B3807D7D"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizontal_bias="0.498"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_bias="0.077" /><Buttonandroid:layout_width="match_parent"android:layout_height="wrap_content"android:onClick="startDownload"android:text="下载"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/imageView1" /></androidx.constraintlayout.widget.ConstraintLayout>

5. 总结

本文我们介绍了一个下载网络图片显示并保存到系统相册的完整小案例,涉及了网络请求、文件下载、动态权限申请、文件保存、移动到系统相册,包含了一些关键实用的知识点:

  • Retrofit 网络请求
  • RxJava 处理网络数据流
  • 动态权限申请
  • 文件保存IO操作
  • ContentResolver 移动文件到系统相册

希望这篇文章能帮到你,欢迎支持,感谢!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 关于k8s的pvc存储卷
  • haproxy七层代理总结
  • SpringBoot参数校验详解
  • PyTorch 基础学习(6)- 自动微分
  • Android Studio设置 offline 模式
  • 解决 Windows 任务栏图标不更新问题:深入解析与解决方案
  • C++初阶_2:引用
  • docker中调用GPU算力(debain12系统)
  • Spring 事务机制
  • Linux中针对文件权限的解析
  • FairyGUI-egret 优化ui资源加载
  • Linux--C语言之输入输出函数及格式控制输出
  • 如何在Shopify开发中高度还原Figma设计稿
  • 操作系统信号集与信号屏蔽
  • 社交工程攻击:心理操纵的艺术与防范策略
  • 自己简单写的 事件订阅机制
  • @jsonView过滤属性
  • 《用数据讲故事》作者Cole N. Knaflic:消除一切无效的图表
  • 【面试系列】之二:关于js原型
  • angular学习第一篇-----环境搭建
  • IIS 10 PHP CGI 设置 PHP_INI_SCAN_DIR
  • iOS 系统授权开发
  • Javascript 原型链
  • JS创建对象模式及其对象原型链探究(一):Object模式
  • Rancher-k8s加速安装文档
  • Stream流与Lambda表达式(三) 静态工厂类Collectors
  • tweak 支持第三方库
  • Vue官网教程学习过程中值得记录的一些事情
  • web标准化(下)
  • 面试总结JavaScript篇
  • 验证码识别技术——15分钟带你突破各种复杂不定长验证码
  • Mac 上flink的安装与启动
  • ​埃文科技受邀出席2024 “数据要素×”生态大会​
  • ​二进制运算符:(与运算)、|(或运算)、~(取反运算)、^(异或运算)、位移运算符​
  • #基础#使用Jupyter进行Notebook的转换 .ipynb文件导出为.md文件
  • (21)起落架/可伸缩相机支架
  • (3)(3.2) MAVLink2数据包签名(安全)
  • (31)对象的克隆
  • (PyTorch)TCN和RNN/LSTM/GRU结合实现时间序列预测
  • (zhuan) 一些RL的文献(及笔记)
  • (附源码)apringboot计算机专业大学生就业指南 毕业设计061355
  • (回溯) LeetCode 46. 全排列
  • (南京观海微电子)——I3C协议介绍
  • (篇九)MySQL常用内置函数
  • (收藏)Git和Repo扫盲——如何取得Android源代码
  • (太强大了) - Linux 性能监控、测试、优化工具
  • .bat批处理(九):替换带有等号=的字符串的子串
  • .bat批处理(五):遍历指定目录下资源文件并更新
  • .desktop 桌面快捷_Linux桌面环境那么多,这几款优秀的任你选
  • .MSSQLSERVER 导入导出 命令集--堪称经典,值得借鉴!
  • .net framework 4.8 开发windows系统服务
  • .NET Remoting Basic(10)-创建不同宿主的客户端与服务器端
  • .NET 设计模式初探
  • .NET 中各种混淆(Obfuscation)的含义、原理、实际效果和不同级别的差异(使用 SmartAssembly)
  • [ 蓝桥杯Web真题 ]-Markdown 文档解析