Skip to content

Commit

Permalink
fix: replace SwipeRefreshLayout's "mTarget" field to avoid content vi…
Browse files Browse the repository at this point in the history
…ew becoming invisible.

Some developer will inject SwipeRefreshLayout's child view after the former is visible. After the FrameLayout is inserted, SwipeRefreshLayout still holds the wrong child reference, which will cause the FrameLayout to not be measured correctly.
  • Loading branch information
nlifew authored and wangaihu committed Jan 12, 2022
1 parent 7e78745 commit b63e0b4
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 2 deletions.
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<activity android:name=".WrapActivity" />
<activity android:name=".ConstraintLayoutActivity" />
<activity android:name=".SetViewActivity" />
</application>
<activity android:name=".SwipeRefreshRecyclerActivity" />
</application>

</manifest>
6 changes: 5 additions & 1 deletion app/src/main/java/com/github/nukc/sample/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,9 @@ public void onClick(View v) {
startActivity(new Intent(MainActivity.this, SetViewActivity.class));
}
});

findViewById(R.id.btn_swipe_recycler).setOnClickListener(v -> {
startActivity(new Intent(MainActivity.this, SwipeRefreshRecyclerActivity.class));
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.github.nukc.sample

import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.github.nukc.stateview.StateView
import kotlin.math.roundToInt

/**
* a example for SwipeRefreshLayout + RecyclerView
*/
class SwipeRefreshRecyclerActivity: AppCompatActivity(), SwipeRefreshLayout.OnRefreshListener {

private class ViewHolderImpl(v: View): RecyclerView.ViewHolder(v)

private class Adapter: RecyclerView.Adapter<ViewHolderImpl>() {


private val mDataSet = ArrayList<String>()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderImpl {
val tv = TextView(parent.context)
tv.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50F, parent.context.resources.displayMetrics).roundToInt()
)
tv.gravity = Gravity.CENTER
return ViewHolderImpl(tv)
}

override fun onBindViewHolder(holder: ViewHolderImpl, position: Int) {
val tv = holder.itemView as TextView
tv.text = mDataSet[position]
}

override fun getItemCount(): Int = mDataSet.size

fun appendDataSet(dataSet: List<String>) {
val from = mDataSet.size

mDataSet.addAll(dataSet)
notifyItemRangeInserted(from, dataSet.size)
}
}


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_swipe_refresh_recycler)


mSwipeRecyclerView = findViewById<SwipeRefreshLayout>(R.id.swipe_refresh).apply {
setOnRefreshListener(this@SwipeRefreshRecyclerActivity)
}


val recyclerView = findViewById<RecyclerView>(R.id.recycler_view).apply {
addItemDecoration(DividerItemDecoration(this@SwipeRefreshRecyclerActivity, DividerItemDecoration.HORIZONTAL))
adapter = mAdapter

}

// start refreshing
mSwipeRecyclerView.isRefreshing = true
onRefresh()

// important:
// we inject into RecyclerView after onResume()
// instead of onCreate(), then the bug occurred !!!

mSwipeRecyclerView.post {
mStateView = StateView.inject(recyclerView)
mStateView.onRetryClickListener = object : StateView.OnRetryClickListener {
override fun onRetryClick() {
mSwipeRecyclerView.isRefreshing = true
onRefresh()
}
}
}
}

private val mHandler = Handler(Looper.getMainLooper())

private val mAdapter = Adapter()

private lateinit var mStateView: StateView
private lateinit var mSwipeRecyclerView: SwipeRefreshLayout


override fun onDestroy() {
super.onDestroy()
mHandler.removeCallbacksAndMessages(null)
}

private var mStateId = 0

override fun onRefresh() {
// success or failure ?

if ((++mStateId and 1) == 1) {
// success
mHandler.postDelayed(1000) {

var idx = 0
val newDataSet = Array(10) { idx++.toString() }

mAdapter.appendDataSet(newDataSet.asList())
mStateView.showContent()
mSwipeRecyclerView.isRefreshing = false
}
}
else {
// ops :(
mHandler.postDelayed(1000) {
mStateView.showRetry()
mSwipeRecyclerView.isRefreshing = false
}
}
}
}

fun Handler.postDelayed(time: Long, action: Runnable) {
postDelayed(action, time)
}
7 changes: 7 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="setEmptyView" />

<Button
android:id="@+id/btn_swipe_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="SwipeRefreshLayout + RecyclerView" />

</LinearLayout>
</ScrollView>
15 changes: 15 additions & 0 deletions app/src/main/res/layout/activity_swipe_refresh_recycler.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
1 change: 1 addition & 0 deletions kotlin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
compileOnly 'androidx.constraintlayout:constraintlayout:2.0.4'
compileOnly 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

Expand Down
27 changes: 27 additions & 0 deletions kotlin/src/main/java/com/github/nukc/stateview/Injector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.NestedScrollingChild
import androidx.core.view.NestedScrollingParent
import androidx.core.view.ScrollingView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.lang.Exception

/**
* @author Nukc.
Expand All @@ -24,6 +26,13 @@ internal object Injector {
false
}

val swipeRefreshLayoutAvailable = try {
Class.forName("androidx.swiperefreshlayout.widget.SwipeRefreshLayout") != null
}
catch (ignore: Throwable) {
false
}

/**
* Create a new FrameLayout (wrapper), let parent's remove children views, and add to the wrapper,
* stateVew add to wrapper, wrapper add to parent
Expand Down Expand Up @@ -139,4 +148,22 @@ internal object Injector {
stateView.stateListAnimator = target.stateListAnimator
}
}

/**
* fix bug: if someone injects SwipeRefreshLayout's child after SwipeRefreshLayout is resumed,
* this child will be hidden.
*/
fun injectIntoSwipeRefreshLayout(layout: SwipeRefreshLayout) {
try {
val mTargetField = SwipeRefreshLayout::class.java.getDeclaredField("mTarget");
mTargetField.isAccessible = true
mTargetField.set(layout, null)

// we replace the mTarget field with 'null', then the SwipeRefreshLayout
// will look for it's real child again.
}
catch (e: Throwable) {
e.printStackTrace()
}
}
}
7 changes: 7 additions & 0 deletions kotlin/src/main/java/com/github/nukc/stateview/StateView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.core.view.NestedScrollingChild
import androidx.core.view.NestedScrollingParent
import androidx.core.view.ScrollingView
import androidx.core.view.ViewCompat
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout

/**
* StateView is an invisible, zero-sized View that can be used
Expand Down Expand Up @@ -340,6 +341,12 @@ class StateView @JvmOverloads constructor(
ViewGroup.LayoutParams.MATCH_PARENT
)
Injector.setStateListAnimator(stateView, view)

// special for SwipeRefreshLayout
if (Injector.swipeRefreshLayoutAvailable && parent is SwipeRefreshLayout) {
Injector.injectIntoSwipeRefreshLayout(parent)
}

return stateView
}
throw ClassCastException("view.getParent() must be ViewGroup")
Expand Down

0 comments on commit b63e0b4

Please sign in to comment.