分享模块的封装
Android 分享包括 QQ 分享、空间分享、微信分享、朋友圈分享、微博分享、系统分享等,好的封装对于添加分享方法的扩建性有很大的帮助。本次封装主要包含分享内容类、分享方式类以及分享弹窗。
ShareContent
分享内容类是分享的根基,一般包括文本内容、图片内容、链接内容,其中所需要的元素也不相同。
- 文本内容:文本
- 图片内容:图片
- 链接内容:图片、标题、描述、链接
密封类
在开始封装前,我们还需要了解一下 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)部分,完成相关工作后就可以开始编写分享的代码了。
-
基础构建:QQ 与空间分享相差不多,所以我们放在同一个体系下面,通过传递参数不同用来控制不同的分享。
class QqShare : BaseShareMethod { private var activity: Activity private var qZone: Boolean private lateinit var params: Bundle constructor(activity: Activity, qZone: Boolean) : super() { this.activity = activity this.qZone = qZone if (qZone) { setIcon(R.drawable.ic_qzone) setName(R.string.qzone) } else { setIcon(R.drawable.ic_qq) setName(R.string.qq) } } companion object { const val Qq_App_Id = "xxxxxxx" } }
-
文本分享:
QQ 空间是不支持纯文本分享的,所以我们文本分享方法仅针对 QQ 自身,如 QQ 好友与 QQ 群的等
override fun textShareMethod(textShareContent: TextShareContent): Boolean { val intent = Intent(Intent.ACTION_SEND) intent.type = "text/plain" intent.putExtra(Intent.EXTRA_TEXT, textShareContent.text) intent.component = ComponentName("com.tencent.mobileqq", "com.tencent.mobileqq.activity.JumpActivity") activity.startActivity(intent) return true }
因为 QQ 空间存在不能分享的类型,不要忘记重写
notShareContent
方法override fun notShareContent(): ArrayList<KClass<out BaseShareContent>> { if (qZone) { return arrayListOf(TextShareContent::class) } return super.notShareContent() }
-
图片与 Url 分享:因为图片分享与 Url 分析代码仅相差一个传递的参数,所以我们将共用的部分单独抽离
override fun urlShareMethod(urlShareContent: UrlShareContent): Boolean { params = Bundle() params.putString(QzoneShare.SHARE_TO_QQ_TITLE, urlShareContent.title) params.putString(QzoneShare.SHARE_TO_QQ_SUMMARY, urlShareContent.message) params.putString(QzoneShare.SHARE_TO_QQ_TARGET_URL, urlShareContent.url) params.putString(QQShare.SHARE_TO_QQ_IMAGE_LOCAL_URL, urlShareContent.getBitmapPath()) return handlerShare() } override fun imageShareMethod(imageShareContent: ImageShareContent): Boolean { params = Bundle() params.putInt(QQShare.SHARE_TO_QQ_KEY_TYPE, QQShare.SHARE_TO_QQ_TYPE_IMAGE) params.putString(QQShare.SHARE_TO_QQ_APP_NAME, StringUtils.getString(R.string.app_name)) params.putString(QQShare.SHARE_TO_QQ_IMAGE_LOCAL_URL, imageShareContent.getBitmapPath()) return handlerShare() } private fun handlerShare(): Boolean { val mTencent = Tencent.createInstance(Qq_App_Id, activity) if (qZone) params.putInt(QQShare.SHARE_TO_QQ_EXT_INT, QQShare.SHARE_TO_QQ_FLAG_QZONE_AUTO_OPEN) // QQ与空间区别参数 mTencent.shareToQQ(activity, params, object : IUiListener { override fun onComplete(p0: Any?) = Unit override fun onError(p0: UiError?) = Unit override fun onCancel() = Unit override fun onWarning(p0: Int) = Unit }) return true }
微信与朋友圈分享
微信相关分享的前提同样是完成 AppId 的申请。在build.gradle
文件下增加implementation 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:6.6.4'
,完成如上工作后,我们还需要在代码内进行配置,详细参照开发文档
-
基础构建:关于微信与朋友圈同样通过传递参数来控制,放在统一体系内。
class WxShare : BaseShareMethod { private val context: Context private var friend: Boolean private lateinit var msg: WXMediaMessage constructor(context: Context, friend: Boolean) : super() { this.context = context this.friend = friend if (friend) { setIcon(R.drawable.ic_friend) setName(R.string.friend) } else { setIcon(R.drawable.ic_wechat) setName(R.string.wx) } } companion object { const val Wx_App_Id = "xxxxxxx" } }
-
分享实现:分享前我们需要注册在代码内注册微信应用,才可以进行分享操作,所以在
preProcessShare
中完成前置处理;然后我们就可以进行分享操作了override fun preProcessShare() { wxApi = WXAPIFactory.createWXAPI(context, Wx_App_Id, false) wxApi.registerApp(Wx_App_Id) } override fun textShareMethod(textShareContent: TextShareContent): Boolean { msg = WXMediaMessage(WXTextObject(textShareContent.text)) // 实际发送内容 msg.description = textShareContent.text // 分享显示内容 return handlerShare() } override fun urlShareMethod(urlShareContent: UrlShareContent): Boolean { msg = WXMediaMessage(WXWebpageObject(urlShareContent.url)) msg.title = urlShareContent.title msg.description = urlShareContent.message msg.setThumbImage(compressBitmap(context, urlShareContent.bitmap, 100f)) // Url 分享图标存在大小限制 return handlerShare() } override fun imageShareMethod(imageShareContent: ImageShareContent): Boolean { msg = WXMediaMessage(WXImageObject(imageShareContent.getBitmap())) return handlerShare() } private fun handlerShare(): Boolean { val req = SendMessageToWX.Req() req.message = msg req.transaction = System.currentTimeMillis().toString() req.scene = if (friend) SendMessageToWX.Req.WXSceneTimeline else SendMessageToWX.Req.WXSceneSession WXEntryActivity.wxApi.sendReq(req) return true }
微博分享
同样的 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("分享失败")
}
})
}
结语
至此,我们的分享模块的封装就完成了,主要记录自己第一次对于面向对象编程的认知,完成了一个小的模块封装。