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

news2024/9/21 17:51:36

文章目录

    • 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.Url

interface INetApi {
    @GET
    fun downloadFile(@Url relativePath: String?): Single<ResponseBody>
}

3.2 Retrofit工具类

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

import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory

object 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: ViewGroup
    private lateinit var imageView1: ImageView

    private val imageUrl = "bfs/archive/961b6650675f97b297bb5ec764ce89b314134544.jpg"
    private val baseImageUrl = "https://i2.hdslb.com/"

    private var disposable: Disposable? = null

    override 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() ?: return
        try {
            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 = contentResolver
            val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return false
            try {
                val outputStream = contentResolver.openOutputStream(uri)?: return false
                val 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.name
            val 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 false
            try {
                val outputStream = contentResolver.openOutputStream(uri)?: return false
                val 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">

    <ImageView
        android: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" />

    <Button
        android: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 移动文件到系统相册

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1994431.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一个简单的录音软件(利用QT录音,ffmpeg进行音频重采样,fdk-aac编码)

录音软件是一种非常有用的工具&#xff0c;可以帮助我们记录和存储语音信息。在本文中&#xff0c;我们将介绍一个简单的录音软件&#xff0c;该软件利用QT进行录音&#xff0c;使用ffmpeg进行音频重采样&#xff0c;并使用fdk-aac编码。 一、 环境介绍 1、QT版本: QT5.…

绕过微信电脑版旧版本限制,版本过低不给登录的问题

这张图&#xff0c;对于还在使用低版本微信电脑版的人很熟悉了吧&#xff01;因为微信逐步开始限制低版本的客户端了&#xff0c;导致无法登陆进去。 为什么这么多人还在使用旧版&#xff1f; 因为很多机器人、框架、HOOK版本的微信等等都是在旧版的基础上开发的&#xff0c;…

嘉立创PCB板子降层(从4层到2层实例)

降层导致的改变 走线和连接&#xff0c;若想正常设计先把要用的内容全部移动到其他层。若不使用可以按照下面方式全部删除。 删除定义使用的规则 删除在需要删除层的走线等所有内容

相同的 LLM 在「不同 GPU 上」会产生不同输出?为什么?

编者按&#xff1a; 在大语言模型(LLMs)的部署及其相关的算力扩容过程中&#xff0c;更换 GPU 是否也可能会对模型的输出产生重大影响&#xff1f;这个问题的答案对于确保 LLMs 在不同硬件环境下的一致性和可靠性至关重要。 我们今天为大家带来的这篇文章&#xff0c;作者的核心…

利用docker部署图形化工具 portainer

docker查找图形化工具 Portainer 拉取镜像 docker pull portainer/portainer启动docker UI容器 docker run -d -p 9209:9000 --name portainer --restart always -v /var/run/docker.sock:/var/run/docker.sock -v /opt/data3/mydocker/portainer_data:/data portainer/porta…

MFC多个控件组合存在显示不出来现象

MFC多个控件组合存在显示不出来现象 1、找到rc文件 &#xff0c; 右键查看代码 2、 3、将基础组件放在最前面即可

C++(week16): C++提高:(六) Qt基础

文章目录 零、课前须知一、Qt基础1.CLI与GUI2.事件驱动模型3.Qt快捷键 二、QtCreator1.Qt的安装&#xff1a;Qt框架、IDE2.Qt的六大模式3.核心模块4.布局5.Qt项目中的文件6.信号与槽机制7.添加资源&#xff1a;资源文件qrc8.main.cpp解析(1)ui文件 和 纯代码(2)按钮 信号槽机制…

在嵌入式Linux平台上使用Nginx搭建RTMP流媒体服务器

概述 Nginx是一个以高效稳定著称的高性能的HTTP和反向代理web服务器&#xff0c;它同时也是基于事件驱动开发的异步高性能跨平台服务器。Nginx-RTMP是基于Nginx框架的模块开发&#xff0c;很好地继承了Nginx的异步高性能以及扩展性好的优点。 RTMP 是 Real Time Messaging Pr…

docker 部署 ElasticSearch;Kibana

ELasticSearch 创建网络 docker network create es-netES配合Kibana使用时需要组网&#xff0c;使两者运行在同一个网络下 命令 docker run -d \ --name es \ -e "discovery.typesingle-node" \ -v /usr/local/es/data:/usr/share/elasticsearch/data \ -v /usr/…

C语言——编译与链接

目录 引言 翻译环境与运行环境 翻译环境 1.翻译环境的简述 2.编译过程 2.1 预处理&#xff08;预编译&#xff09; 2.2 编译 2.2.1 词法分析 2.2.2 语法分析 2.2.3 语义分析 2.3 汇编 3.链接 运行环境 结束语 引言 C语言编译与链接过程是理解程序如何从代码转化…

8月5日学习笔记 glibc安装与安全用户角色权限

一&#xff0c;glibc安装 https://www.mysql.com/ 官⽹ https://downloads.mysql.com/archives/community/ https://downloads.mysql.com/archives/get/p/23/file/mysql-8.0.33-li nux-glibc2.12-x86_64.tar 安装步骤 1.安装依赖库 [rootlocalhost ~]# yum list installed |g…

在vscode中使用ssh运行docker:从下载到运行全流程

首先在本机或者服务器上下载docker并运行 本文目的旨在本机下载docker并打包&#xff0c;然后在服务器上进行加载 docker -v Docker version 27.0.3, build 7d4bcd8有输出说明在运行 一、下载 在docker hub上下载docker以tensorflow为例 点击tag搜索自己想要的版本 copy命…

All-Reduce通信原语;Reduce+LayerNorm+Broadcast算子;gRPC:远程过程调用(RPC)框架;

目录 All-Reduce通信原语 定义与作用 实例说明 示例图解(以Ring算法为例) 结论 Reduce+LayerNorm+Broadcast算子 1. Reduce算子 2. LayerNorm算子 3. Broadcast算子 组合使用场景 gRPC:远程过程调用(RPC)框架 All-Reduce通信原语 是计算机科学中,特别是在分布式…

解锁成都跃享未来教育咨询抖音小店

在数字化浪潮汹涌的今天&#xff0c;教育行业的变革与创新层出不穷&#xff0c;其中&#xff0c;成都跃享未来教育咨询以其敏锐的洞察力和前瞻性的教育理念&#xff0c;在抖音平台上开设的小店&#xff0c;正悄然改变着人们的学习方式和教育资源的获取途径。本文将深入探讨成都…

骰子游戏的UML分析

一、需求分析 游戏者掷两个骰子,如果总点数是7则赢得游戏,否则为输 二、概要设计 2.1 设计用例 用例是基于某个场景(注:包括成功和失败场景,重要体现需求的边界)说明了用户如何通过系统实现实现其目标。 示例:玩游戏场景用例 用例名称:玩游戏 主要参与者: 游戏用户 前…

实时数据监控,三防平板在工业领域的应用解析

随着工业4.0时代的到来&#xff0c;数字化转型已成为各行各业的共同目标。在这一过程中&#xff0c;实时数据监控扮演着至关重要的角色&#xff0c;为企业提供数据驱动的决策支持&#xff0c;提升效率、降低成本、提高安全性。而作为移动终端设备&#xff0c;三防平板凭借其可靠…

深兰科技荣获2024年度金势奖“AI出海先锋品牌”金奖

近日&#xff0c;由金势奖组委会、凤凰网、营销国际协会等国内外知名机构、集团共同主办的“第四届未来营销大会暨锐品牌盛典”在上海举行。大会揭晓了第四届“金势奖锐品牌大赏”奖项的评选结果&#xff0c;深兰科技凭借自身在机器人产品出口和海外市场开拓等出海全球化发展方…

2-59 基于matlab的全离散法单自由度稳定极限切深叶瓣图绘制、两自由度稳定极限切深叶瓣图绘制

基于matlab的全离散法单自由度稳定极限切深叶瓣图绘制、两自由度稳定极限切深叶瓣图绘制&#xff0c;特定切削力系数进行数值积分。输出相应的叶瓣图。程序已调通&#xff0c;可直接运行。 2-59 两自由度稳定极限切深叶瓣图 - 小红书 (xiaohongshu.com)

【计算机组成原理】3.程序的执行

程序的执行 进程与线程 一个程序&#xff0c;读入内存&#xff0c;全是0和1构成 从内存读入到CPU计算&#xff0c;这个时候要通过总线 怎么区分一段01的数据到底是数据还是指令&#xff1f; 总线分类为三种&#xff1a;控制线 地址线 数据线 一个程序的执行&#xff0c;首…

美团创始人的亲授产品课

2020年王慧文受邀在清华大学演讲了个人的非产品公开课&#xff0c;课程内容以美团早期的实战经验结合规模效益、马太理论等诸多知名理论为主。 前两天重新翻阅的时候&#xff0c;还是有很多新的感悟&#xff0c;所以也借此机会把课程内容和大家分享一下。 规模效益 一个业务有…