本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!
引子
项目中参数多级透传满天飞的状况很常见,增加了开发的复杂度、犯错的或许、及保护的的难度。
透传包括两种办法:
- 不同界面之间参数透传。
- 同一界面中不同层级控件间透传。
该系列的目标是消除这两种参数透传,使得不同界面以及同一界面内各层级间愈加解耦,降低参数传递开发的复杂度,削减犯错的或许,增加可保护性。
上一篇经过向前查询参数的办法解决了第一个 case,本篇先聚焦在第二个 case,即同一界面不同层级控件间的参数透传。
透传举例
比方下面这个场景:
特效卡片的点击事情需求传入私参“type”,以表明属于哪个 tab 页。
界面层级如下:素材集市用 EffectActivity 来承载,其间的标签栏下方是一个子 Fragment ,其间包含了 ViewPager 控件,该控件内部的每一个页又是一个 Fragment。
埋点私参和上报机遇分处于两个不同的页面层级。上报机遇在最内层 Fragment 触发,而私参在 Activity 层级生成,遂需经过两层 Fragment 的透传。
所以就会呈现如下代码:
// EffcetActivity.kt
override fun showEffectListContent(index: Int, from: String?) {
mVpEffectContent.adapter = SimpleFragmentStatePagerAdapter(supportFragmentManager).apply {
mTitles = arrayOf("视频库", "音乐", "音效", "贴纸", "转场", "特效", "滤镜", "布景", "字幕", "字体")
mCount = mTitles!!.size
createFragment = { position ->
when (position) {
0 -> {
val fragment = RemoteCenterFragment.newInstance( -1, true, 0, MaterialProtocol.SOURCE.MATERIAL_MARKET, 1, from, 0, 0)
val paramsParser = DeepLinkParamsParser(compileDeepLinkParams())
(fragment as IDeepLinkPage).setDeepLinkParams(paramsParser.deeplinkParams)
fragment
}
1 -> {
MaterialMusicFragment.newInstance(from,mSelectedTabId,mSelectedModelId)
}
2 -> {
MaterialAudioFragment.newInstance(mSelectedTabId,mSelectedModelId)
}
// 索引值到类型值的映射
3 -> {
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_VSTICKER)
}
4 -> {
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_TRANSITION)
}
5 -> {
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_EFFECT)
}
6 -> {
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_FILTER)
}
7 -> {
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_BACKGROUND)
}
8 -> {
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_SUBTITLE)
}
else -> {
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_FONT)
}
}
}
}
mVpEffectContent.currentItem = index
mVpEffectContent.offscreenPageLimit = 10
mTlEffectTabs.setupWithViewPager(mVpEffectContent)
}
EffectListFragment
即使承载 ViewPager 的 Fragment,上述代码在构建其实例时做了分类讨论,目的是为了根据不同类型的 tab 透传相应的 type 值。
EffectListFragment 不得不先承受透传参数并持续传递到下一个层级:
class EffectListFragment : BaseMvpFragment{
// 保存透传参数的变量
private var mCurrentType: Int = -1
override fun initConfig(savedInstanceState: Bundle?) {
// 获取透传参数
mCurrentType = arguments?.getInt(CommonConstant.EFFECTCENTER.TYPE) ?: 0
selectHotTab()
showEffectDetails()
}
override fun showEffectDetails() {
mVpEffectDetails?.adapter = SimpleFragmentPagerAdapter(childFragmentManager).apply {
mCount = 1
createFragment = { position ->
// 参数持续透传到下一个层级
EffectDetailsFragment.newInstance(mCurrentType, CommonConstant.EFFECTCENTER.ORDER_HOT).also {
currentFragments[0] = it
}
}
}
}
companion object {
fun newInstance(type: Int): EffectListFragment {
// 参数透传
return EffectListFragment().apply {
arguments = Bundle().apply { putInt(CommonConstant.EFFECTCENTER.TYPE, type) }
}
}
}
终究承受并消费透传 type 的是 EffectDetailsFragment,即纵向翻滚列表的承载页:
class EffectDetailsFragment : BaseMvpFragment<EffectDetailsContract.IView, EffectDetailsPresenter>() {
// 保存透传参数的变量
private var mCurrentType: Int = 0
override fun initConfig(savedInstanceState: Bundle?) {
// 承受透传参数
mCurrentType = arguments?.getInt(CommonConstant.EFFECTCENTER.TYPE) ?: 0
mOrder = arguments?.getInt(ORDER) ?: 0
initDetailsContent()
}
override fun initEvent() {
mTvNetworkRetry.setOnClickListener(this)
mEffectDetailsAdapter.addOnItemClickListener(object : EffectDetailsAdapter.OnItemClickListener {
override fun onItemClick(entity: EffectDataEntity?) {
entity?.let {
// 消费透传参数,上报埋点
StudioReport.reportClickAlbumClick(it.type, it.id, mOrder, "all", mCurrentType)
}
}
})
}
companion object {
const val ORDER: String = "order"
fun newInstance(type: Int, order: Int): EffectDetailsFragment {
return EffectDetailsFragment().apply {
// 接纳透传
arguments = Bundle().apply {
putInt(CommonConstant.EFFECTCENTER.TYPE, type)
putInt(ORDER, order)
}
}
}
}
}
整个界面层级以及参数传递途径如下:
Activity 中有一个 Fragment,而它内部又嵌套了一个 Fragment。
中间的 Fragment 很无辜,由于它并不需求消费 type 参数,而只是做一个快递员。
当时只有两层,假如层级再增多,因此而增加的复杂度和工作量让人难以承受。
向上查询
假如把上述传参的办法叫做 “自顶向下透传” 的话,下面要介绍的这个计划能够称为 “自底向上查询”。
自顶向下透传是容易完成的,由于父亲总是持有孩子的引用,向孩子注入参数轻而易举。
有没有一种计划能够完成反向的参数查询,即当孩子触发埋点事情时,逐级向上查询父亲生成的参数。
Android 中的控件是持有父亲的:
// android.view.View.java
public final ViewParent getParent() {
return mParent;
}
经过一个循环不停地获取当时控件的父控件,就能从 View 树的叶子结点遍历到树根:
var viewParent: View?
do {
viewParent = viewParent?.parent as? View
} while(viewParent != null)
关于 Activity 来说,树根便是 DecorView。关于 Fragment 来说,树根便是 onCreateView() 中创建的视图。
Fragment 终究会以一个 View 的办法嵌入到 Activity 的 View 树中。所以关于当个 Activity 来说,不管嵌套几层 Fragment,其视图结构终究都能够归为一棵 View 树。
怎么让 Activity View 树中的每一个控件都能带着事务参数?
需求界说一个接口:
// 可盯梢的结点
interface TrackNode {
fun fillTrackParams(): HashMap<String, String>?
}
为 View 新增一个扩展属性,让每个控件都持有一个 TrackNode:
var View.trackNode: TrackNode?
get() = this.getTag(R.id.spm_id_tag) as? TrackNode
set(value) {
this.setTag(R.id.spm_id_tag, value)
}
将带着参数的才能存放在 View.tag 中,这样任何控件都能够带着参数了。
让 Activity 带着参数体现为让其根视图 DecorView 带着参数:
// 在所有 Activity 的基类中完成 TrackNode,则所有 Activity 都具有了带着参数的才能
open class BaseActivity : AppCompatActivity(), TrackNode{
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Activity 带着参数表现为其根视图带着参数
window.decorView.rootView.trackNode = this
}
override fun fillTrackParams(): HashMap<String, String>? {
return null
}
}
同样地,Fragment 也有类似的完成:
open class BaseFragment : Fragment(), TrackNode {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Fragment 带着参数表现为其根视图带着参数
getView()?.trackNode = this
}
override fun fillTrackParams(): HashMap<String, String>? {
return null
}
}
这样一来,一个窗口中整个 View 树的任何一个结点都具有了带着参数的才能,从最顶层的 Activity,到其内部的 Fragment,再到任何一个控件。参数就不再需求自顶向下透传,而是能够自底向上查询:
fun View.getTrackNode(): HashMap<String, String> {
val map = hashMapOf<String, String>()
// 获取当时结点的参数
trackNode?.fillTrackParams()?.also { map.putAll(it) }
// 不断获取父亲以向上查询
var viewParent = parent as? View
do {
// 查询父控件是否带着参数
val info = viewParent?.trackNode?.fillTrackParams()
// 若父控件带着参数则将其拼接
info?.also { map.putAll(it) }
// 持续获取父控件
viewParent = viewParent?.parent as? View
} while (viewParent != null) // 直到回溯到了整个界面的根视图
return map
}
为 View 自界说了一个扩展办法,该办法回来一个 Map,该 Map 中包含了从当时界面向上到树根整个链路中所有带着的参数调集。
重构透传
先在 Activity 层级将标签页的 type 拼接到 TrackNode 中,而不是作为参数传递给 EffectListFragment:
// EffectActivity.kt
override fun showEffectListContent(index: Int, from: String?) {
mVpEffectContent.adapter = SimpleFragmentStatePagerAdapter(supportFragmentManager,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT).apply {
mTitles = arrayOf("视频库", "音乐", "音效", "贴纸", "转场", "特效", "滤镜", "布景", "字幕", "字体")
mCount = mTitles!!.size
createFragment = { position ->
when (position) {
0 -> {
val fragment = RemoteCenterFragment.newInstance(-1,true,0,MaterialProtocol.SOURCE.MATERIAL_MARKET,1,from,0,0)
val paramsParser = DeepLinkParamsParser(compileDeepLinkParams())
(fragment as IDeepLinkPage).setDeepLinkParams(paramsParser.deeplinkParams)
fragment
}
1 -> MaterialMusicFragment.newInstance(from, mSelectedTabId, mSelectedModelId)
2 -> MaterialAudioFragment.newInstance(mSelectedTabId, mSelectedModelId)
// 能够无差别地构造 EffectListFragment 实例
else -> EffectListFragment.newInstance()
}
}
}
mVpEffectContent.currentItem = index
mVpEffectContent.offscreenPageLimit = 10
mTlEffectTabs.setupWithViewPager(mVpEffectContent)
}
// 索引和常量的映射
private val tabMap = mapOf(
3 to CommonConstant.SERVER.TYPE_VSTICKER,
4 to CommonConstant.SERVER.TYPE_TRANSITION,
5 to CommonConstant.SERVER.TYPE_EFFECT,
6 to CommonConstant.SERVER.TYPE_FILTER,
7 to CommonConstant.SERVER.TYPE_BACKGROUND,
8 to CommonConstant.SERVER.TYPE_SUBTITLE,
9 to CommonConstant.SERVER.TYPE_FONT,
)
// Activity 层级的参数拼接
override fun fillTrackParams(): HashMap<String, String>? {
// 只拼接当时显示页的常量
return hashMapOf("type" to tabMap[mVpEffectContent.currentItem].toString()
}
第二个层级的 Fragment 在构建实例时不再承受传参(愈加单纯):
class EffectListFragment : BaseFragment() {
companion object {
// 没有参数传入的构造办法
fun newInstance(): EffectListFragment = EffectListFragment()
}
}
在最内层的 Fragment 消费参数:
class EffectDetailsFragment : BaseFragment(){
// 向上查参
private val type: Int
get() = view?.getTrackNode()?.getOrElse("type") { "" }?.safeToInt() ?: 0
override fun initEvent() {
mEffectDetailsAdapter.addOnItemClickListener(object : EffectDetailsAdapter.OnItemClickListener {
override fun onItemClick(entity: EffectDataEntity?) {
// 消费参数进行埋点
entity?.let {
ReportUtil.reportClick(it.id, type)
}
}
})
}
companion object {
// 没有type 传入的构造办法
fun newInstance(): EffectDetailsFragment = EffectDetailsFragment()
}
}
消费参数时不再经过上一个界面透传,而是经过自底向上的查询。
由于约好的参数是 HashMap<String, String> 类型的,而消费的参数是 Int 类型的所以得进行类型转化
假如强制的运用如下办法进行转化,则或许发生运行时溃散,比方下面这个场景:
" " as Int
为了防止这类溃散,有必要做一个一致处理:
fun String?.safeToInt(): Int = this?.let {
try {
Integer.parseInt(this)
} catch (e: NumberFormatException) {
e.printStackTrace()
0
}
} ?: 0
为 String 界说一个扩展办法,该办法回来 Int 值,在内部调用Integer.parseInt(this)
将当时的 String 转化为 Int,并在其外层包裹了 try-catch 以捕获非数字字串转化反常的状况。
运用 Kotlin 中的预界说let()
办法配合try-catch
表达式以及 Evis 运算符,让这个办法的表达反常简洁。
其间let()
的界说如下:
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
let 也是一个扩展办法,被扩展目标是泛型,表明它能够被任何目标调用。let 接纳一个 lambda,该 lambda 会将类型 T 变换为 类型 R,let 办法内部只是经过block(this)
履行了该 lambda 并回来,遂 let 的回来值便是 lambda 的值(lambda 终究一条句子的值)。
从 let 的界说能够看出,它一般用于将一个目标转化为另一个目标。当时场景中它被用于将 String 转化为 Int。
String 转化为 Int 是或许抛反常的,遂用 try-catch 包裹之。Kotlin 中try-catch
是一个表达式,它是有值的,等于每个分支终究一条句子的值。这个特性使得不必多声明一个局部变量:
int result = 0;
try {
result = Integer.parseInt(str);
} catch (Exception e) {
result = -1
}
return result;
所以整个 safeToInt() 的回来值是 let 的回来,而 let 的回来值是 try-catch 的回来值。
终究由于被扩展的目标是 String?,所以回来值是可空的,办法内部经过?:
处理了这种状况。表达式1 ?: 表达式2
意思是当表达式1为空时,履行表达式2。
适用场景
自底向上查询参数计划适用于同一窗口的任何层级之间的参数传递。
当在 Fragment 中向上查询时,要在onCreateView()
之后,由于在此之前,Fragment 的视图层级还未生成,getView()
会回来 null。
RecyclerView 中 ItemView 无法运用自底向上查询,由于ItemView.parent
为空。
能够在 inflate ItemView 布局时将 attachToRoot 设置为 true:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(
R.layout.material_item_effect_details,
parent,
true)// 将 attachToRoot 设置为 true
return ViewHolder(itemView)
}
这样 ItemView 的 parent 就不为空了,可是 ItemView 的 LayoutParam 就会被其父控件的 LayoutParam 掩盖,使得 ItemView 的布局款式不符合预期。
列表项参数透传解决计划
那列表相关的参数透传途径就必定得是 Activity -> Adapter -> ViewHolder ?
Adapter 的语义是完成数据到视图的转化。ViewHolder 的语义是描绘怎么构建表项视图及其交互。
假如将透传逻辑和表项的构建及交互逻辑耦合在一起,除了增加了透传参数的复杂度,还使得后者无法被独立复用。
更好的做法是将表项的曝光和点击事情上移到 Activity/Fragment 处理,为此新增了两个扩展法办法:
fun RecyclerView.setOnItemClickListener(listener: (View, Int, Float, Float) -> Boolean) {
addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
val gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener {
override fun onShowPress(e: MotionEvent?) {
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
e?.let {
findChildViewUnder(it.x, it.y)?.let { child ->
val realX = if (child.left >= 0) it.x - child.left else it.x
val realY = if (child.top >= 0) it.y - child.top else it.y
return listener( child, getChildAdapterPosition(child), realX, realY )
}
}
return false
}
override fun onDown(e: MotionEvent?): Boolean {
return false
}
override fun onFling( e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float ): Boolean {
return false
}
override fun onScroll( e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float ): Boolean {
return false
}
override fun onLongPress(e: MotionEvent?) {
}
})
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {
}
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
gestureDetector.onTouchEvent(e)
return false
}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
}
})
}
经过判断触点坐标落在 RecyclerView 的哪个孩子上进行点击事情的回调。详细剖析能够点击读源码长知识 | 更好的 RecyclerView 表项点击监听器
以及 RecyclerView 表项百分比曝光扩展办法:
fun RecyclerView.onItemVisibilityChange(percent: Float = 0.5f, block: (itemView: View, adapterIndex: Int, isVisible: Boolean) -> Unit) {
val rect = Rect() // reuse rect object rather than recreate it everytime for a better performance
val visibleAdapterIndexs = mutableSetOf<Int>()
val scrollListener = object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
// iterate all children of RecyclerView to check whether it is visible
for (i in 0 until childCount) {
val child = getChildAt(i)
val adapterIndex = getChildAdapterPosition(child)
val childVisibleRect = rect.also { child.getLocalVisibleRect(it) }
val visibleArea = childVisibleRect.let { it.height() * it.width() }
val realArea = child.width * child.height
if (visibleArea >= realArea * percent) {
if (visibleAdapterIndexs.add(adapterIndex)) {
block(child, adapterIndex, true)
}
} else {
if (adapterIndex in visibleAdapterIndexs) {
block(child, adapterIndex, false)
visibleAdapterIndexs.remove(adapterIndex)
}
}
}
}
}
addOnScrollListener(scrollListener)
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
}
override fun onViewDetachedFromWindow(v: View?) {
if (v == null || v !is RecyclerView) return
if (ViewCompat.isAttachedToWindow(v)) {
v.removeOnScrollListener(scrollListener)
}
removeOnAttachStateChangeListener(this)
}
})
}
经过监听列表翻滚事情,并在其间遍历列表所有的孩子,同时计算每个孩子矩形区域在列表中展示的百分比判断其可见性,详细剖析能够点击
总结
经过思路的改变,将“自顶向下透传参数”改变为“自顶向上查询参数”,降低了同一界面层级中各控件之间的耦合,使得每个控件都愈加单纯。
引荐阅读
事务代码参数透传满天飞?(一)
事务代码参数透传满天飞?(二)
全网最优雅安卓控件可见性检测
全网最优雅安卓列表项可见性检测
页面曝光难点剖析及应对计划
你的代码太烦琐了 | 这么多目标名?
你的代码太烦琐了 | 这么多办法调用?