用AI实现图片分享,对方在浏览器打开预览的demo
用AI实现图片分享功能
Demo的需求,本质上是:
Android 端选择/生成一张图片 → 上传到服务器 → 生成一个公网 URL → 把这个 URL 分享给别人 →
对方在手机浏览器中打开并预览图片
这个方案比单纯 Intent 分享本地图片 更适合“跨手机浏览器预览”。
一、先说明一个关键点
仅靠 Android 本地代码,不能直接让“对方浏览器打开预览”
因为浏览器预览需要一个可访问的网络地址,所以必须有:
一个服务器
或图片 / 对象存储
或你自己的上传接口
也就是说,这个 Demo 至少包含两部分:
1)Android 客户端
负责:
选择图片
上传图片
获取 URL
分享 URL
2)服务端
负责:
接收上传的图片
保存图片
返回可访问链接
二、我给你的 Demo 方案
为了让你更容易跑通,我给你一个最简单可运行结构:
技术方案
Android 端
Kotlin
Retrofit 上传图片
FileProvider / 相册选择
分享链接
服务端
我这里先给你一个最简单 Node.js Demo 方案
因为它容易搭建、代码少、适合测试
三、完整流程
Android App 选择一张图片
点击上传
上传到服务器
服务器保存图片
返回图片访问地址,例如:
http://你的IP:3000/uploads/abc123.jpg
Android 把这个地址分享出去
对方在浏览器中打开这个链接
浏览器直接显示图片
四、注意:局域网测试和公网测试
1)局域网测试
如果两台手机和电脑在同一 Wi-Fi 下,可以用你电脑的局域网 IP:
例如:
http://192.168.1.100:3000/uploads/test.jpg
这样同一局域网内的其他手机浏览器可以访问。
2)公网测试
如果你希望任意手机都能访问,需要:
部署到云服务器
或用内网穿透工具
或对象存储/CDN
五、第二版 Demo:Android 客户端完整代码
1. 功能说明
这个 Android Demo 实现:
-
从相册选择图片
-
预览图片
-
上传图片到服务器
-
返回图片 URL
-
分享图片 URL 给其他手机
-
其他手机浏览器打开即可预览
2. app/build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.example.imageuploaddemo'
compileSdk 35
defaultConfig {
applicationId "com.example.imageuploaddemo"
minSdk 24
targetSdk 35
versionCode 1
versionName "1.0"
}
buildFeatures {
viewBinding true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}
3. AndroidManifest.xml
如果你上传网络图片,需要网络权限:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:label="ImageUploadDemo"
android:supportsRtl="true"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
4. activity_main.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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="250dp"
android:layout_height="250dp"
android:background="#EEEEEE"
android:scaleType="centerCrop"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/btnPick"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="选择图片"
app:layout_constraintTop_toBottomOf="@id/imageView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp" />
<Button
android:id="@+id/btnUpload"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="上传图片"
app:layout_constraintTop_toBottomOf="@id/btnPick"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="12dp" />
<TextView
android:id="@+id/tvUrl"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="上传后的链接会显示在这里"
android:textSize="14sp"
android:padding="8dp"
android:background="#F5F5F5"
app:layout_constraintTop_toBottomOf="@id/btnUpload"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp" />
<Button
android:id="@+id/btnShareUrl"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="分享链接"
app:layout_constraintTop_toBottomOf="@id/tvUrl"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="12dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
5. UploadResponse.kt
package com.example.imageuploaddemo
data class UploadResponse(
val success: Boolean,
val url: String,
val message: String? = null
)
6. ApiService.kt
package com.example.imageuploaddemo
import okhttp3.MultipartBody
import retrofit2.Call
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
interface ApiService {
@Multipart
@POST("/upload")
fun uploadImage(
@Part file: MultipartBody.Part
): Call<UploadResponse>
}
7. MainActivity.kt
package com.example.imageuploaddemo
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.example.imageuploaddemo.databinding.ActivityMainBinding
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.asRequestBody
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.File
import java.io.FileOutputStream
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var selectedImageUri: Uri? = null
private var uploadedImageUrl: String? = null
// 这里替换成你的服务端地址
// 局域网测试示例: http://192.168.1.100:3000/
private val baseUrl = "http://192.168.1.100:3000/"
private val apiService: ApiService by lazy {
val client = OkHttpClient.Builder().build()
Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
private val pickImageLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
if (uri != null) {
selectedImageUri = uri
binding.imageView.setImageURI(uri)
binding.tvUrl.text = "已选择图片,点击上传"
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnPick.setOnClickListener {
pickImageLauncher.launch("image/*")
}
binding.btnUpload.setOnClickListener {
if (selectedImageUri == null) {
Toast.makeText(this, "请先选择图片", Toast.LENGTH_SHORT).show()
} else {
uploadImage(selectedImageUri!!)
}
}
binding.btnShareUrl.setOnClickListener {
if (uploadedImageUrl.isNullOrEmpty()) {
Toast.makeText(this, "请先上传图片并获取链接", Toast.LENGTH_SHORT).show()
} else {
shareUrl(uploadedImageUrl!!)
}
}
}
private fun uploadImage(uri: Uri) {
try {
val file = uriToFile(uri)
val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
binding.tvUrl.text = "上传中..."
apiService.uploadImage(body).enqueue(object : Callback<UploadResponse> {
override fun onResponse(
call: Call<UploadResponse>,
response: Response<UploadResponse>
) {
if (response.isSuccessful && response.body() != null) {
val result = response.body()!!
if (result.success) {
uploadedImageUrl = result.url
binding.tvUrl.text = result.url
Toast.makeText(this@MainActivity, "上传成功", Toast.LENGTH_SHORT).show()
} else {
binding.tvUrl.text = "上传失败:${result.message ?: "未知错误"}"
}
} else {
binding.tvUrl.text = "上传失败:服务器返回异常"
}
}
override fun onFailure(call: Call<UploadResponse>, t: Throwable) {
binding.tvUrl.text = "上传失败:${t.message}"
}
})
} catch (e: Exception) {
e.printStackTrace()
binding.tvUrl.text = "上传异常:${e.message}"
}
}
private fun shareUrl(url: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, url)
}
startActivity(Intent.createChooser(intent, "分享链接"))
}
private fun uriToFile(uri: Uri): File {
val fileName = getFileName(uri) ?: "upload_image.jpg"
val tempFile = File(cacheDir, fileName)
contentResolver.openInputStream(uri)?.use { inputStream ->
FileOutputStream(tempFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
return tempFile
}
private fun getFileName(uri: Uri): String? {
var name: String? = null
val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
cursor?.use {
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (it.moveToFirst() && nameIndex >= 0) {
name = it.getString(nameIndex)
}
}
return name
}
}
六、服务端 Demo(Node.js 版)
下面这个服务端很简单,用来本地测试很方便。
1. 新建文件夹
比如:
E:\image-upload-server
2. 初始化项目
在该目录打开命令行,执行:
npm init -y
npm install express multer cors
3. 新建 server.js
const express = require("express");
const multer = require("multer");
const cors = require("cors");
const path = require("path");
const fs = require("fs");
const app = express();
const PORT = 3000;
app.use(cors());
app.use("/uploads", express.static(path.join(__dirname, "uploads")));
if (!fs.existsSync("uploads")) {
fs.mkdirSync("uploads");
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "uploads/");
},
filename: function (req, file, cb) {
const ext = path.extname(file.originalname);
const uniqueName = Date.now() + "_" + Math.floor(Math.random() * 10000) + ext;
cb(null, uniqueName);
},
});
const upload = multer({ storage });
app.post("/upload", upload.single("file"), (req, res) => {
if (!req.file) {
return res.json({
success: false,
url: "",
message: "未接收到文件",
});
}
const serverIp = getLocalIp();
const fileUrl = `http://${serverIp}:${PORT}/uploads/${req.file.filename}`;
res.json({
success: true,
url: fileUrl,
message: "上传成功",
});
});
function getLocalIp() {
const os = require("os");
const interfaces = os.networkInterfaces();
for (let name in interfaces) {
for (let iface of interfaces[name]) {
if (iface.family === "IPv4" && !iface.internal) {
return iface.address;
}
}
}
return "127.0.0.1";
}
app.listen(PORT, "0.0.0.0", () => {
console.log(`Server running at http://0.0.0.0:${PORT}`);
});
4. 启动服务端
执行:
node server.js
启动成功后,服务会监听:
http://你的电脑局域网IP:3000
比如:
http://192.168.1.100:3000
七、Android 端要改哪里
把 MainActivity.kt 里的:
private val baseUrl = "http://192.168.1.100:3000/"
改成你自己电脑真实的局域网 IP。
八、如果 Android 9+ 无法访问 HTTP
Android 9 以后默认限制明文 HTTP。
如果你的服务端是 http://,你需要额外配置。
1. AndroidManifest.xml 的 application 加:
android:usesCleartextTraffic="true"
完整示例:
<application
android:allowBackup="true"
android:label="ImageUploadDemo"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
九、运行方式
步骤
电脑启动 Node.js 服务端
手机和电脑连接同一 Wi-Fi
Android App 里把 baseUrl 改成电脑 IP
运行 App
选择图片
上传图片
得到 URL
分享 URL 给其他手机
其他手机浏览器打开链接即可预览
十、效果示例
假设上传成功返回:
http://192.168.1.100:3000/uploads/1716871234567_1234.jpg
别人收到这个链接后,在浏览器打开,就会直接看到图片。
十一、这个 Demo 的局限
这个版本适合:
-
本地测试
-
局域网预览
-
原型验证
不适合直接上线,因为缺少:
-
图片大小限制
-
文件类型校验
-
安全认证
-
HTTPS
-
CDN
-
数据库存储
-
防盗链
十二、如果你想做正式版,建议方案
正式环境一般会用:
Spring Boot + OSS/MinIO
Node.js + 对象存储
七牛云 / 阿里云 OSS / 腾讯云 COS
Firebase Storage
这样返回的是公网地址,其他手机不在同一个 Wi-Fi 下也能打开。
更多推荐



所有评论(0)