我们应该怎样接受其他APP的分享的照片、视频、文本、链接或者其他类型的文件呢?即如下图效果,让我们的APP也出现在分享列表之中:
本文将介绍,如何将我们flutter开发的APP也出现在分享列表之中。
下面我们将分成3部分介绍:
1.Android和iOS平台的配置
2.Flutter端的实现
3.编译问题及实现
原生端的配置
安卓配置
首先,我们在AndroidManifest.xml中增加些 intent filters,用来接收其他APP的分享文件。
代码语言:javascript复制<activity>
...
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
...
</activity>
android:name=”android.intent.action.SEND
: 接收单个文件
android:name=”android.intent.action.SEND_MULTIPLE
: 接收多个文件
android:mimeType=”<Type>/*”
. 类型:[文本、图像、视频,/(任意)]
我们还需要添加*android:launchMode=”singleTask”
: 作为独立的任务启动。*
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
...
</activity>
在onCreate中加入如下代码:
代码语言:javascript复制package com.flutter.kaifajingxuan.receivesharing
import android.content.Intent
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import android.os.Bundle
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
java.lang.Thread.sleep(1000);
}
override fun onCreate(savedInstanceState: Bundle?) {
if (intent.getIntExtra("org.chromium.chrome.extra.TASK_ID", -1) == this.taskId) {
this.finish()
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
super.onCreate(savedInstanceState)
}
}
安卓的配置到此结束。
iOS的配置:
首先,我们需要在info.plist
中添加相关类型的权限。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia</string>
</array>
</dict>
<dict/>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSPhotoLibraryUsageDescription</key>
<string>ReceiveSharing requires the Photos Permission to select and upload the photo</string>
</dict>
</plist>
接下来,还需要配置为我们的APP田间share extension
,具体步骤如下:
- 选择File -> New ->Target.
2. 搜索Share -> Share Extension->Next.
3. 依次设置Product Name:「Share Extension」 > Team :选择你自己的team ->Finish.
4. Runner. app中Deployment target选择9.0或更高
5. 依次选择Share Extension -> Info.plist -> Open as -> Source Code
6.添加以下的关键代码
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
<integer>15</integer>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>15</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>15</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
<key>PHSupportedMediaTypes</key>
<array>
<string>Video</string>
<string>Image</string>
</array>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>
❝PHSupportedMediaTypes : video & image
NSExtensionActivationSupportsText
: TextNSExtensionActivationSupportsWebURLWithMaxCount
: No of Urls to shareNSExtensionActivationSupportsImageWithMaxCount
: No of Images to shareNSExtensionActivationSupportsMovieWithMaxCount
: No of Movies to shareNSExtensionActivationSupportsFileWithMaxCount
: No of Files to share ❞
7. 在ShareViewController.swift
中添加如下关键代码:
import UIKit
import Social
import MobileCoreServices
import Photos
class ShareViewController: SLComposeServiceViewController {
let hostAppBundleIdentifier = "com.jp.receivesharing"
let sharedKey = "ShareKey"
var sharedMedia: [SharedMediaFile] = []
var sharedText: [String] = []
let imageContentType = kUTTypeImage as String
let videoContentType = kUTTypeMovie as String
let textContentType = kUTTypeText as String
let urlContentType = kUTTypeURL as String
let fileURLType = kUTTypeFileURL as String;
override func isContentValid() -> Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad();
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
if let contents = content.attachments {
for (index, attachment) in (contents).enumerated() {
if attachment.hasItemConformingToTypeIdentifier(imageContentType) {
handleImages(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(textContentType) {
handleText(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(fileURLType) {
handleFiles(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(urlContentType) {
handleUrl(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(videoContentType) {
handleVideos(content: content, attachment: attachment, index: index)
}
}
}
}
}
override func didSelectPost() {
print("didSelectPost");
}
override func configurationItems() -> [Any]! {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
return []
}
private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in
if error == nil, let item = data as? String, let this = self {
this.sharedText.append(item)
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.(this.hostAppBundleIdentifier)")
userDefaults?.set(this.sharedText, forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .text)
}
} else {
self?.dismissWithError()
}
}
}
private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in
if error == nil, let item = data as? URL, let this = self {
this.sharedText.append(item.absoluteString)
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.(this.hostAppBundleIdentifier)")
userDefaults?.set(this.sharedText, forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .text)
}
} else {
self?.dismissWithError()
}
}
}
private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in
if error == nil, let url = data as? URL, let this = self {
// Always copy
let fileName = this.getFileName(from: url, type: .image)
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.(this.hostAppBundleIdentifier)")!
.appendingPathComponent(fileName)
let copied = this.copyFile(at: url, to: newPath)
if(copied) {
this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image))
}
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.(this.hostAppBundleIdentifier)")
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .media)
}
} else {
self?.dismissWithError()
}
}
}
private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: videoContentType, options: nil) { [weak self] data, error in
if error == nil, let url = data as? URL, let this = self {
// Always copy
let fileName = this.getFileName(from: url, type: .video)
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.(this.hostAppBundleIdentifier)")!
.appendingPathComponent(fileName)
let copied = this.copyFile(at: url, to: newPath)
if(copied) {
guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else {
return
}
this.sharedMedia.append(sharedFile)
}
// If this is the last item, save imagesData in userDefaults and redirect to host app
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.(this.hostAppBundleIdentifier)")
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .media)
}
} else {
self?.dismissWithError()
}
}
}
private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) { [weak self] data, error in
if error == nil, let url = data as? URL, let this = self {
// Always copy
let fileName = this.getFileName(from :url, type: .file)
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.(this.hostAppBundleIdentifier)")!
.appendingPathComponent(fileName)
let copied = this.copyFile(at: url, to: newPath)
if (copied) {
this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file))
}
if index == (content.attachments?.count)! - 1 {
let userDefaults = UserDefaults(suiteName: "group.(this.hostAppBundleIdentifier)")
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
userDefaults?.synchronize()
this.redirectToHostApp(type: .file)
}
} else {
self?.dismissWithError()
}
}
}
private func dismissWithError() {
print("[ERROR] Error loading data!")
let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert)
let action = UIAlertAction(title: "Error", style: .cancel) { _ in
self.dismiss(animated: true, completion: nil)
}
alert.addAction(action)
present(alert, animated: true, completion: nil)
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
private func redirectToHostApp(type: RedirectType) {
let url = URL(string: "ShareMedia://dataUrl=(sharedKey)#(type)")
var responder = self as UIResponder?
let selectorOpenURL = sel_registerName("openURL:")
while (responder != nil) {
if (responder?.responds(to: selectorOpenURL))! {
let _ = responder?.perform(selectorOpenURL, with: url)
}
responder = responder!.next
}
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
enum RedirectType {
case media
case text
case file
}
func getExtension(from url: URL, type: SharedMediaType) -> String {
let parts = url.lastPathComponent.components(separatedBy: ".")
var ex: String? = nil
if (parts.count > 1) {
ex = parts.last
}
if (ex == nil) {
switch type {
case .image:
ex = "PNG"
case .video:
ex = "MP4"
case .file:
ex = "TXT"
}
}
return ex ?? "Unknown"
}
func getFileName(from url: URL, type: SharedMediaType) -> String {
var name = url.lastPathComponent
if (name.isEmpty) {
name = UUID().uuidString "." getExtension(from: url, type: type)
}
return name
}
func copyFile(at srcURL: URL, to dstURL: URL) -> Bool {
do {
if FileManager.default.fileExists(atPath: dstURL.path) {
try FileManager.default.removeItem(at: dstURL)
}
try FileManager.default.copyItem(at: srcURL, to: dstURL)
} catch (let error) {
print("Cannot copy item at (srcURL) to (dstURL): (error)")
return false
}
return true
}
private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? {
let asset = AVAsset(url: forVideo)
let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded()
let thumbnailPath = getThumbnailPath(for: forVideo)
if FileManager.default.fileExists(atPath: thumbnailPath.path) {
return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video)
}
var saved = false
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
assetImgGenerate.appliesPreferredTrackTransform = true
// let scale = UIScreen.main.scale
assetImgGenerate.maximumSize = CGSize(width: 360, height: 360)
do {
let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil)
try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath)
saved = true
} catch {
saved = false
}
return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) : nil
}
private func getThumbnailPath(for url: URL) -> URL {
let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "")
let path = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.(hostAppBundleIdentifier)")!
.appendingPathComponent("(fileName).jpg")
return path
}
class SharedMediaFile: Codable {
var path: String; // can be image, video or url path. It can also be text content
var thumbnail: String?; // video thumbnail
var duration: Double?; // video duration in milliseconds
var type: SharedMediaType;
init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) {
self.path = path
self.thumbnail = thumbnail
self.duration = duration
self.type = type
}
// Debug method to print out SharedMediaFile details in the console
func toString() {
print("[SharedMediaFile] ntpath: (self.path)ntthumbnail: (self.thumbnail)ntduration: (self.duration)nttype: (self.type)")
}
}
enum SharedMediaType: Int, Codable {
case image
case video
case file
}
func toData(data: [SharedMediaFile]) -> Data {
let encodedData = try? JSONEncoder().encode(data)
return encodedData!
}
}
extension Array {
subscript (safe index: UInt) -> Element? {
return Int(index) < count ? self[Int(index)] : nil
}
}
hostAppBundleIdentifier
: 你的包名(package name)
8. 将Runner 和 Share Extension加入到相同的group中。
9.选择Capabilities tab -> App Groups -> Add a new group, 都取名为 group.<host-bundle-indentifier>
比如group.flutter.kaifajingxuan.receivesharing
10. 两个taget最终都有相同的group。
编译问题及修复
- 检查share extension下的 **Build Settings,**并且移除
Linking/Other Linker Flags
下的所有选项。 - disabled
extension target
的bitcode选项。 - invalid Bundle. 设置路径‘Runner. app/Plugins/.appex contains disallowed file ‘Frameworks’。我的设置如下:
Always Embed Swift Standard Libraries: YES
share extension 的设置如下
Always Embed Swift Standard Libraries: NO
flutter端的实现
我们需要先引入一个 receive_sharing_intent插件
代码语言:javascript复制dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
receive_sharing_intent: ^1.4.5
demo 一共有3个页面
- Home Screen: 接收其他 apps的文件
- User Listing Screen: 选择你要分享文件的用户
- Sharing Media Preview Screen: 要分享文件的预览页面
「home_screen.dart」的实现如下:
代码语言:javascript复制//All listeners to listen Sharing media files & text
void listenShareMediaFiles(BuildContext context) {
// For sharing images coming from outside the app
// while the app is in the memory
ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) {
navigateToShareMedia(context, value);
}, onError: (err) {
debugPrint("$err");
});
// For sharing images coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
navigateToShareMedia(context, value);
});
// For sharing or opening urls/text coming from outside the app while the app is in the memory
ReceiveSharingIntent.getTextStream().listen((String value) {
navigateToShareText(context, value);
}, onError: (err) {
debugPrint("$err");
});
// For sharing or opening urls/text coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialText().then((String? value) {
navigateToShareText(context, value);
});
}
void navigateToShareMedia(BuildContext context, List<SharedMediaFile> value) {
if (value.isNotEmpty) {
var newFiles = <File>[];
value.forEach((element) {
newFiles.add(File(
Platform.isIOS
? element.type == SharedMediaType.FILE
? element.path
.toString()
.replaceAll(AppConstants.replaceableText, "")
: element.path
: element.path,
));
});
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => UserListingScreen(
files: newFiles,
text: "",
)));
}
}
void navigateToShareText(BuildContext context, String? value) {
if (value != null && value.toString().isNotEmpty) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => UserListingScreen(
files: [],
text: value,
)));
}
}
❝getMediaStream : 当APP在后台运行时接收分享的媒体文件 getInitialMedia : 当APP被杀掉时接收分享的媒体文件 getTextStream : 当APP在后台运行时接收分享的文本 getInitialText : 当APP被杀掉时接收分享的文本 ❞
在 initState
中调用listenShareMediaFiles
方法.
「user_listing_screen」.dart的代码如下:
代码语言:javascript复制class UserListingScreen extends StatefulWidget {
final List<File>? files;
final String? text;
UserListingScreen({this.files, this.text = ""});
@override
_UserListingScreenState createState() => _UserListingScreenState();
}
class _UserListingScreenState extends State<UserListingScreen> {
List<UserDetailModel> _userNames = [
UserDetailModel(
name: "Harsh", email: "harsh.dev@gmail.com", isSelected: false),
UserDetailModel(
name: "Jaimil", email: "jaimil.dev@gmail.com", isSelected: false),
UserDetailModel(
name: "Piyush", email: "piyush.dev@gmail.com", isSelected: false),
UserDetailModel(
name: "Niket", email: "niket.dev@gmail.com", isSelected: false),
UserDetailModel(
name: "Shailin", email: "shailin.dev@gmail.com", isSelected: false),
UserDetailModel(
name: "Nishat", email: "nishat.dev@gmail.com", isSelected: false),
];
bool _isSelected = false;
List<String?> _selectedNames = [];
@override
Widget build(BuildContext context) {
return Column(
children: [_userListingView(context), _selectedUserListingView(context)],
).generalScaffold(
context: context,
appTitle: "Users",
isShowFab: _isSelected,
files: widget.files,
userList: _userNames,
sharedText: widget.text);
}
Widget _userListingView(BuildContext context) => Expanded(
child: ListView.builder(
itemCount: _userNames.length,
itemBuilder: (context, index) {
return _userListItemView(context, index);
}));
Widget _userListItemView(BuildContext context, int index) => Card(
elevation: 3,
child: ListTile(
selected: _userNames[index].isSelected,
selectedTileColor: ColorConstants.primaryColor,
dense: true,
onTap: () {
_onListTileTap(index);
},
leading: _leadingCircularView(index),
title: _titleView(index),
subtitle: _subTitleView(index),
),
);
Widget _leadingCircularView(int index) => CircleAvatar(
backgroundColor: _userNames[index].isSelected
? ColorConstants.whiteColor
: ColorConstants.primaryColor,
child: Text(
_userNames[index].name!.substring(0, 1),
style: TextStyle(
color: _userNames[index].isSelected
? ColorConstants.primaryColor
: ColorConstants.whiteColor),
));
Widget _titleView(int index) => Text(_userNames[index].name!,
style: TextStyle(
color: _userNames[index].isSelected
? ColorConstants.whiteColor
: ColorConstants.primaryColor));
Widget _subTitleView(int index) => Text(_userNames[index].email!,
style: TextStyle(color: ColorConstants.greyColor));
Widget _selectedUserListingView(BuildContext context) => (_selectedNames
.isNotEmpty)
? Container(
height: DimensionConstants.containerHeight50,
decoration:
BoxDecoration(color: ColorConstants.whiteColor, boxShadow: [
BoxShadow(offset: Offset(0, -3), blurRadius: 5, color: Colors.grey)
]),
padding: EdgeInsets.symmetric(
horizontal: DimensionConstants.horizontalPadding10),
child: ListView.builder(
itemCount: _selectedNames.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Center(
child: Text(
"${"${_selectedNames[index]}"}${index == _selectedNames.length - 1 ? "" : " , "}",
style: TextStyle(
fontSize: FontSizeWeightConstants.fontSize14,
fontWeight:
FontSizeWeightConstants.fontWeight500)));
}))
: SizedBox();
void _onListTileTap(int index) {
setState(() {
_userNames[index].isSelected = !_userNames[index].isSelected;
for (var names in _userNames) {
if (names.isSelected) {
if (!_selectedNames.contains(names.name)) {
_selectedNames.add(names.name);
}
} else {
_selectedNames.remove(names.name);
}
}
_isSelected = _selectedNames.isNotEmpty;
});
}
}
sharing_media_preview_screen.dart的代码如下
代码语言:javascript复制class SharingMediaPreviewScreen extends StatefulWidget {
final List<UserDetailModel>? userList;
final List<File>? files;
final String? text;
SharingMediaPreviewScreen({this.userList, this.files, this.text = ""});
@override
_SharingMediaPreviewScreenState createState() =>
_SharingMediaPreviewScreenState();
}
class _SharingMediaPreviewScreenState extends State<SharingMediaPreviewScreen> {
final PageController _pageController =
PageController(initialPage: 0, viewportFraction: 0.95, keepPage: false);
final List<MediaPreviewItem> _galleryItems = [];
int _initialIndex = 0;
@override
void initState() {
super.initState();
SchedulerBinding.instance?.addPostFrameCallback((timeStamp) {
setState(() {
var i = 0;
widget.files?.forEach((element) {
_galleryItems.add(MediaPreviewItem(
id: i,
resource: element,
controller: TextEditingController(),
isSelected: i == 0 ? true : false));
i ;
});
});
});
}
@override
Widget build(BuildContext context) {
return _galleryItems.isNotEmpty
? Column(
children: [
SizedBox(height: DimensionConstants.sizedBoxHeight5),
_fullMediaPreview(context),
],
).generalScaffold(
context: context,
appTitle: "Send to...",
files: widget.files,
userList: widget.userList)
: widget.text!.isNotEmpty
? _sharedTextView(context).generalScaffold(
context: context,
appTitle: "Send to...",
files: widget.files,
userList: widget.userList)
: EmptyView(
topLine: "No files are here..",
bottomLine: "Select files from gallery or file manager.",
).generalScaffold(
context: context,
appTitle: "Send to...",
files: widget.files,
userList: widget.userList);
}
Widget _fullMediaPreview(BuildContext context) => Expanded(
child: PageView(
controller: _pageController,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.horizontal,
onPageChanged: (value) {
_mediaPreviewChanged(value);
},
children: _galleryItems
.map((e) => AppConstants.imageExtensions
.contains(e.resource?.path.split('.').last.toLowerCase())
? Image.file(File(e.resource!.path))
: Image.asset(
FileConstants.icFile,
))
.toList(),
));
void _mediaPreviewChanged(int value) {
_initialIndex = value;
setState(() {
var i = 0;
_galleryItems.forEach((element) {
if (i == value) {
_galleryItems[i].isSelected = true;
} else {
_galleryItems[i].isSelected = false;
}
i ;
});
});
}
}
最后,我们再给sharing_media_preview_screen添加以下分享的描述就OK了,最终如下;
代码语言:javascript复制class SharingMediaPreviewScreen extends StatefulWidget {
final List<UserDetailModel>? userList;
final List<File>? files;
final String? text;
SharingMediaPreviewScreen({this.userList, this.files, this.text = ""});
@override
_SharingMediaPreviewScreenState createState() =>
_SharingMediaPreviewScreenState();
}
class _SharingMediaPreviewScreenState extends State<SharingMediaPreviewScreen> {
...
@override
Widget build(BuildContext context) {
return _galleryItems.isNotEmpty
? Column(
children: [
SizedBox(height: DimensionConstants.sizedBoxHeight5),
_fullMediaPreview(context),
_fileName(context),
_addCaptionPreview(context),
_horizontalMediaFilesView(context)
],
).generalScaffold(
context: context,
appTitle: "Send to...",
files: widget.files,
userList: widget.userList)
: widget.text!.isNotEmpty
? _sharedTextView(context).generalScaffold(
context: context,
appTitle: "Send to...",
files: widget.files,
userList: widget.userList)
: EmptyView(
topLine: "No files are here..",
bottomLine: "Select files from gallery or file manager.",
).generalScaffold(
context: context,
appTitle: "Send to...",
files: widget.files,
userList: widget.userList);
}
...
Widget _fileName(BuildContext context) => Padding(
padding: const EdgeInsets.all(DimensionConstants.padding8),
child: Text(
"${_galleryItems[_initialIndex].resource!.path.split('/').last}"),
);
Widget _addCaptionPreview(BuildContext context) => Row(children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: DimensionConstants.leftPadding15,
right: DimensionConstants.rightPadding20,
top: DimensionConstants.topPadding10),
child: TextFormField(
controller: _galleryItems[_initialIndex].controller,
textInputAction: TextInputAction.done,
focusNode: FocusNode(),
style: TextStyle(
color: ColorConstants.blackColor,
fontSize: FontSizeWeightConstants.fontSize14,
fontWeight: FontSizeWeightConstants.fontWeightNormal),
decoration: InputDecoration(
hintText: "Add Caption",
hintStyle: TextStyle(
color: ColorConstants.blackColor,
fontSize: FontSizeWeightConstants.fontSize14,
fontWeight:
FontSizeWeightConstants.fontWeightNormal),
filled: true,
fillColor: ColorConstants.offWhiteColor,
counter: Offstage(),
contentPadding: EdgeInsets.symmetric(
horizontal: DimensionConstants.horizontalPadding5),
border: InputBorder.none),
onFieldSubmitted: (value) {},
keyboardType: TextInputType.text,
onTap: () {}))),
GestureDetector(
onTap: () {
_onSharingTap(context);
},
child: Padding(
padding: const EdgeInsets.only(
bottom: DimensionConstants.bottomPadding8),
child: Image.asset(FileConstants.icSend, scale: 2.7)))
]);
Widget _horizontalMediaFilesView(BuildContext context) =>
(MediaQuery.of(context).viewInsets.bottom == 0)
? Container(
height: DimensionConstants.containerHeight60,
margin: const EdgeInsets.only(
left: DimensionConstants.leftPadding15,
bottom: DimensionConstants.bottomPadding10,
top: DimensionConstants.topPadding5),
child: ListView.separated(
itemCount: _galleryItems.length,
separatorBuilder: (context, index) {
return SizedBox(width: DimensionConstants.sizedBoxWidth10);
},
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_onTapHorizontalMedia(context, index);
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: _galleryItems[index].isSelected
? ColorConstants.greyColor
: ColorConstants.whiteColor,
width: 1.0)),
child: AppConstants.imageExtensions.contains(
_galleryItems[index]
.resource
?.path
.split('.')
.last
.toLowerCase())
? Image.file(
File(_galleryItems[index].resource!.path))
: Image.asset(FileConstants.icFile)));
},
scrollDirection: Axis.horizontal))
: SizedBox();
void _onTapHorizontalMedia(BuildContext context, int index) {
setState(() {
var i = 0;
_galleryItems.forEach((element) {
if (i == index) {
_galleryItems[i].isSelected = true;
} else {
_galleryItems[i].isSelected = false;
}
i ;
});
});
_pageController.animateToPage(index,
duration: Duration(milliseconds: 400), curve: Curves.easeIn);
}
Widget _sharedTextView(BuildContext context) =>
Column(mainAxisAlignment: MainAxisAlignment.end, children: [
Text("Shared text here...",
style: TextStyle(
color: ColorConstants.greyColor,
fontSize: FontSizeWeightConstants.fontSize20)),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: DimensionConstants.horizontalPadding10),
child: Row(children: [
Text(widget.text!,
style:
TextStyle(fontSize: FontSizeWeightConstants.fontSize20)),
Spacer(),
GestureDetector(
onTap: () {
_onSharingTap(context);
},
child: Padding(
padding: const EdgeInsets.only(
bottom: DimensionConstants.bottomPadding8),
child: Image.asset(FileConstants.icSend, scale: 2.7)))
]))
]);
void _onSharingTap(BuildContext context) {
//You can use this method to share media file or text based on your requirements
}
}
安卓效果
总结
我们实现一个接收分享文件的app,就像微信的分享功能一样,虽然样式很丑,但功能还是可以的,
github的地址:https://github.com/JaimilPatel/ReceiveSharing
少年别走,交个朋友~