分享模块的封装

​ Android 分享包括 QQ 分享、空间分享、微信分享、朋友圈分享、微博分享、系统分享等,好的封装对于添加分享方法的扩建性有很大的帮助。本次封装主要包含分享内容类、分享方式类以及分享弹窗。

ShareContent

​ 分享内容类是分享的根基,一般包括文本内容、图片内容、链接内容,其中所需要的元素也不相同。

  1. 文本内容:文本
  2. 图片内容:图片
  3. 链接内容:图片、标题、描述、链接

密封类

​ 在开始封装前,我们还需要了解一下 Kotlin 的密封类。密封类某种意义上属于枚举类型的拓展,表示受限的类的继承结构,使用sealed修饰符。密封类的子类必须与密封类声明在同一个文件,且密封类是抽象类不能直接实例化,可以有抽象abstract成员。密封类的优点在于使用when表达式的同时,无需添加else字段,且可以验证子类的覆盖性。

基类构建

​ 为什么使用密封类呢?因为对于分享内容,后期我们可能存在其余扩展,当在密封类内添加扩展的内容子类后,编译器自动帮助我们校验需要写扩展代码的地方,避免疏忽遗漏导致程序出现问题。

​ 针对于图片,我们在使用的时候通常会使用到图片的绝对路径与Bitmap,所以我们在需要图片的地方写两个方法来获取信息,同时因为图片内容和链接内容都需要图片,所以我们可以抽象出来,避免写两次同样的内容。

sealed class BaseShareContent {		// 密封类
    data class TextShareContent(
        var text: String,
    ) : BaseShareContent()

    data class UrlShareContent(
        override var bitmap: Uri,
        var title: String,
        var message: String,
        var url: String,
    ) : BaseShareContent(), BitmapContent

    data class ImageShareContent(
        override var bitmap: Uri,
    ) : BaseShareContent(), BitmapContent
}

interface BitmapContent {
    var bitmap: Uri

    fun getBitmapPath(): String {
        return UriUtils.uri2File(bitmap).absolutePath
    }

    fun getBitmap(): Bitmap {
        return BitmapFactory.decodeFile(getBitmapPath())
    }
}

ShareMethod

基类构建

​ 分享内容完成后,我们则需要分享方式类。对于分享方式我们包含的 icon 图标、分享名称、分享内容等,所以我们可以写一个抽象类包含这些属性以被所有的分享方式类继承。

abstract class BaseShareMethod {
    private lateinit var icon: Drawable
    private lateinit var name: String

    fun setIcon(icon: Int) {
        this.icon = ResourceUtils.getDrawable(icon)
    }

    fun setIcon(icon: Drawable) {
        this.icon = icon
    }

    fun setName(name: Int) {
        this.name = StringUtils.getString(name)
    }

    fun setName(name: String) {
        this.name = name
    }

    fun getIconDrawable(): Drawable {
        return icon
    }

    fun getNameString(): String {
        return name
    }
}

​ 接下来我们还需要一个分享的方法,根据分享内容的类型不同会调用不同的方法。

/**
* 分享方法
*/
fun share(baseShareContent: BaseShareContent): Boolean {
    preProcessShare()
    return when (baseShareContent) {
        is UrlShareContent -> urlShareMethod(baseShareContent as UrlShareContent)
        is ImageShareContent -> imageShareMethod(baseShareContent as ImageShareContent)
        is TextShareContent -> textShareMethod(baseShareContent as TextShareContent)
    }
}

/**
* 分享内容预处理
*/
open fun preProcessShare() {}

abstract fun textShareMethod(textShareContent: TextShareContent): Boolean

abstract fun urlShareMethod(urlShareContent: UrlShareContent): Boolean

abstract fun imageShareMethod(imageShareContent: ImageShareContent): Boolean

​ 此时我们就可以发现在分享内容封装时使用密封类的优势,当每当增加一种分享内容的时候,该方法中的when会在编译时自动监测没有覆盖到的类型,已保证程序不会出错。

​ 还需要注意的地方是,并不是每种分享方式都可以分享所有内容。例如,我们没有办法通过调用官方 APi 使用 QQ空间分享纯文本,系统分享也无法分享 URL,所以我们还需要增加一个判断该分享方式是否支持分享内容的一个方法。

/**
* 判断是否支持该分享方法
*/
fun isSupportContentType(baseShareContent: BaseShareContent): Boolean {
    return notShareContent().indexOfFirst { shareContentClass -> return@indexOfFirst baseShareContent::class == shareContentClass } == -1
}

/**
* 不支持分享的类型
*/
protected open fun notShareContent(): ArrayList<KClass<out BaseShareContent>> {
    return arrayListOf()
}

​ 首先我们写一个子类继承方法notShareContent用来返回不能分享的内容类型(class),然后增加一个外部可调用的isSupportContentType方法,用来判断当前类的分享内容是否包含在不可分享内容类型中,返回布尔值。

​ 至此我们分享方式的基类就完成了,下面我们开始实现具体的分享方法。

QQ 与空间分享

​ QQ 相关的分享前提我们是已经完成 AppId 的申请。我们需要前往腾讯开放平台下载 SDK,具体 SDK 的引入可查看开发文档中的(2)(3)部分,完成相关工作后就可以开始编写分享的代码了。

微信与朋友圈分享

​ 微信相关分享的前提同样是完成 AppId 的申请。在build.gradle文件下增加implementation 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:6.6.4',完成如上工作后,我们还需要在代码内进行配置,详细参照开发文档

微博分享

​ 同样的 AppId 要准备好,微博 SDK 下载地址。具体实现就不再详细解释了,参照代码。

class WbShare : BaseShareMethod {

    private val activity: Activity

    private lateinit var msg: WeiboMultiMessage

    constructor(activity: Activity) : super() {
        this.activity = activity

        setIcon(R.drawable.ic_weibo)
        setName(R.string.weibo)
    }

    companion object {
        const val Weibo_App_Id = "xxxxxxx"
        const val Weibo_Redirect_Url = "https://api.weibo.com/oauth2/default.html"
    }

    override fun preProcessShare() {
        WbSdk.install(activity, AuthInfo(activity, Weibo_App_Id, Weibo_Redirect_Url, ""))
        msg = WeiboMultiMessage()
    }

    override fun textShareMethod(textShareContent: TextShareContent): Boolean {
        val textObj = TextObject()
        textObj.text = textShareContent.text
        msg.textObject = textObj
        return handlerShare()
    }

    override fun urlShareMethod(urlShareContent: UrlShareContent): Boolean {
        val webObj = WebpageObject()
        webObj.title = urlShareContent.title
        webObj.description = urlShareContent.message
        webObj.setThumbImage(urlShareContent.getBitmap())
        webObj.actionUrl = urlShareContent.url
        msg.mediaObject = webObj
        return handlerShare()
    }

    override fun imageShareMethod(imageShareContent: ImageShareContent): Boolean {
        val imgObj = ImageObject()
        imgObj.imagePath = imageShareContent.getBitmapPath()
        msg.imageObject = imgObj
        return handlerShare()
    }

    private fun handlerShare(): Boolean {
        val shareHandler = WbShareHandler(activity)
        shareHandler.registerApp()

        shareHandler.shareMessage(msg, false)
        return true
    }

系统分享

​ 需要注意的是系统分享无法分享 Url,所以不要忘记重写notShareContent方法

class SystemShare : BaseShareMethod {

    private val context: Context

    constructor(context: Context) : super() {
        this.context = context

        setIcon(R.drawable.ic_horizontal_more)
        setName(R.string.more)
    }

    override fun notShareContent(): ArrayList<KClass<out BaseShareContent>> {
        return arrayListOf(UrlShareContent::class)
    }

    override fun textShareMethod(textShareContent: TextShareContent): Boolean {
        val intent = Intent(Intent.ACTION_SEND)
        intent.type = "text/plain"
        intent.putExtra(Intent.EXTRA_TEXT, textShareContent.text)
        context.startActivity(intent)
        return true
    }

    override fun urlShareMethod(urlShareContent: UrlShareContent): Boolean {
        return false
    }

    override fun imageShareMethod(imageShareContent: ImageShareContent): Boolean {
        val intent = Intent(Intent.ACTION_SEND)
        intent.type = "image/*"
        intent.putExtra(Intent.EXTRA_STREAM, imageShareContent.bitmap)
        context.startActivity(intent)
        return true
    }
}

ShareDialog

​ 我们通过 Dialog 自定义布局将所有的分享方式展现出来,具体居中左对齐布局实现方法我们在后期发布,大家可以使用 RecyclerView 进行布局。

基础构建

class ShareBottomDialog : BottomDialogFragment {

    private val binding by bindView(DialogShareBinding::inflate)	// 自定义的委托绑定布局,大家使用自己的方式

    var baseShareContent: BaseShareContent
        private set

    private var shareMethod: ArrayList<BaseShareMethod>	// 所有分享方法
    
    private lateinit var ableShareMethod: ArrayList<BaseShareMethod>	// 可用分享方法

    constructor(baseShareContent: BaseShareContent) : super() {
        this.baseShareContent = baseShareContent
        
        shareMethod = arrayListOf(	// 按照想要的循序加入
            WxShare(requireContext(), false),
            QqShare(requireActivity(), false),
            WbShare(requireActivity()),
            WxShare(requireContext(), true),
            QqShare(requireActivity(), true),
            SystemShare(requireContext())
        )
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return binding.root
    }
}

分享方法筛选

​ 通过调用isSupportContentType方法来判断分享方式是否支持当前的分享内容

private fun refreshShareList() {
    ableShareMethod = arrayListOf()
    shareMethod.forEach { method -> if (method.isSupportContentType(baseShareContent)) ableShareMethod.add(method) }
}

设置数据

​ 一切准备就绪后我们可以开始填充数据,下面是我写的自定义布局的实现方式,与 RecyclerView 的 Adapter 差不多,可以参照代码。

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)

    refreshShareList()

    binding.slvShareList.setView(R.layout.item_share)	// 布局
    binding.slvShareList.setData(ableShareMethod, object : ShareListView.ChildHolder<BaseShareMethod> {
        override fun setData(data: BaseShareMethod, view: View) {
            view.findViewById<ImageView>(R.id.img_share_ic).setImageDrawable(data.getIconDrawable())	// 为 item 子布局设置内容
            view.findViewById<TextView>(R.id.tv_share_name).text = data.getNameString()
        }
    })
    binding.slvShareList.setOnItemClickListener(object : ShareListView.OnClickListener<BaseShareMethod> {	// 子布局点击事件
        override fun onClick(v: View, position: Int, data: BaseShareMethod) {
            data.setShareContent(baseShareContent)
            if (!data.share()) toast("分享失败")
        }
    })
}

结语

​ 至此,我们的分享模块的封装就完成了,主要记录自己第一次对于面向对象编程的认知,完成了一个小的模块封装。