flutter-如何实现类型微信分享的功能

2022-09-20 16:48:25 浏览数 (1)

我们应该怎样接受其他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” : 作为独立的任务启动。*

代码语言:javascript复制
<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中添加相关类型的权限。

代码语言: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>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 ,具体步骤如下:

  1. 选择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 : Text NSExtensionActivationSupportsWebURLWithMaxCount : No of Urls to share NSExtensionActivationSupportsImageWithMaxCount : No of Images to share NSExtensionActivationSupportsMovieWithMaxCount : No of Movies to share NSExtensionActivationSupportsFileWithMaxCount : No of Files to share ❞

7. 在ShareViewController.swift中添加如下关键代码:

代码语言:javascript复制
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。

编译问题及修复

  1. 检查share extension下的 **Build Settings,**并且移除Linking/Other Linker Flags下的所有选项。
  2. disabled extension target 的bitcode选项。
  3. 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个页面

  1. Home Screen: 接收其他 apps的文件
  2. User Listing Screen: 选择你要分享文件的用户
  3. 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

少年别走,交个朋友~

0 人点赞