Skip to content

Instantly share code, notes, and snippets.

@devahmedshendy
Last active August 31, 2020 11:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save devahmedshendy/ec726e56965b9812b19a7adb22c87345 to your computer and use it in GitHub Desktop.
Save devahmedshendy/ec726e56965b9812b19a7adb22c87345 to your computer and use it in GitHub Desktop.
USE CASE: Transition Background of Selected Item From Previous Selected Item (Sample: https://youtu.be/2c9Hjo211yE , Demo: https://youtu.be/xrGats2_rBc)
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
private lateinit var mBinding: ActivityMainBinding
private lateinit var mNamesAdapter: NamesAdapter
// MARK: Lifecycle Methods
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupActivity()
}
// MARK: Setup Methods
private fun setupActivity() {
hideStatusBar()
mBinding = DataBindingUtil.setContentView(this@MainActivity, R.layout.activity_main)
mNamesAdapter = NamesAdapter(mBinding.maHighlightV)
mBinding.maMomNamesRv.layoutManager = LinearLayoutManager(this@MainActivity)
mBinding.maMomNamesRv.adapter = mNamesAdapter
mBinding.maMomNamesRv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
(mNamesAdapter as OnRecyclerViewScrollCallback).onScroll()
}
})
mNamesAdapter.update(buildMomNameList())
}
private fun hideStatusBar() {
val uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN
window.decorView.systemUiVisibility = uiOptions
}
// MARK: Helper Methods
private fun buildMomNameList(): List<String> {
return listOf(
"Momzilla", "Maa", "Ammi", "MuNu", "Amma", "MooMoo",
"Life", "Paradise", "Heaven", "Queen", "Fairy", "Momey",
"Mommu", "Mumu", "Maaaaa", "Heartbeat", "Wonder Woman", "Yo Mama",
"SuperWoman", "Big Mama"
)
}
}
/**
* RecyclerView Adapter Class
* It receives HighlightBackground view, so it can handle all operations required
* when user select/deselect any NameHolder.
*/
class NamesAdapter(private val mHighlightBackground: View) :
RecyclerView.Adapter<NamesAdapter.NameHolder>(), OnRecyclerViewScrollCallback {
companion object {
private const val TAG = "MomNamesAdapter"
}
private val FADE_DURATION = 150L
private val SLOW_DURATION = 500L
private val BLINK_DURATION = 0L
private val mList = mutableListOf<String>()
// To store current selected Name and NameHolder
private var mCurrentSelectedName: String? = null
private var mCurrentSelectedHolder: View? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NameHolder {
val binding = HolderNameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return NameHolder(binding)
}
override fun getItemCount(): Int {
return mList.size
}
override fun onBindViewHolder(holder: NameHolder, position: Int) {
val name = mList[position]
holder.mBinding.name = name
holder.mBinding.root.setOnClickListener{ onSelection(name, it) }
}
fun update(list: List<String>) {
mList.clear()
mList.addAll(list)
notifyDataSetChanged()
}
class NameHolder(val mBinding: HolderNameBinding) : RecyclerView.ViewHolder(mBinding.root)
// MARK: All Next For Operations on HighlightBackground Once User Select/Deselect a NameHolder
/**
* When NameHolder gets detached, we check if this NameHolder is currently selected,
* if so then hide HighlightBackground, and DON'T clear current selection.
*/
override fun onViewDetachedFromWindow(holder: NameHolder) {
super.onViewDetachedFromWindow(holder)
when {
isSameCurrentSelection(holder.mBinding.name!!, holder.mBinding.root) -> {
hideHighlightBackground(clearCurrentSelections = false)
}
}
}
/**
* When NameHolder gets attached, we check if this NameHolder is currently selected,
* if so then show HighlightBackground.
*/
override fun onViewAttachedToWindow(holder: NameHolder) {
super.onViewAttachedToWindow(holder)
when {
isSameCurrentSelection(holder.mBinding.name!!, holder.mBinding.root) -> {
showHighlightBackground()
}
}
}
/**
* For each scroll step happening, we check if there is a current selection,
* if so then we need to fix the position of Highlight Background.
*/
override fun onScroll() {
when {
!isCurrentSelectionNull() -> {
fixHighlightBackgroundPosition()
}
}
}
/**
* This just sets X & Y for Highlight Background to new values of current selection.
*/
private fun fixHighlightBackgroundPosition() {
val (newX, newY, _, _) = calculateNewPositionValues()
mHighlightBackground.x = newX
mHighlightBackground.y = newY
}
/**
* This is to be called by onClickListener of NameHolder, which acts as selecting current NameHolder.
*/
private fun onSelection(selectedName: String, selectedHolder: View) {
when {
/*
* Fresh selections.
* We need to update current selections, then show Highlight Background
*/
isCurrentSelectionNull() -> {
mCurrentSelectedName = selectedName
mCurrentSelectedHolder = selectedHolder
showHighlightBackground()
}
/*
* Current selections is selected again.
* We need to hide the Highlight Background, but also ask to clear current selections
*/
isSameCurrentSelection(selectedName, selectedHolder) -> {
hideHighlightBackground(clearCurrentSelections = true)
}
/*
* New selections than current ones.
* We need to update current selections, then - this time - move (Animate) the
* Highlight Background to position of the new selections.
*/
isDifferentSelection(selectedName, selectedHolder) -> {
mCurrentSelectedName = selectedName
mCurrentSelectedHolder = selectedHolder
moveHighlightBackground()
}
}
}
private fun isCurrentSelectionNull(): Boolean {
return mCurrentSelectedName == null && mCurrentSelectedHolder == null
}
private fun isSameCurrentSelection(selectedName: String, selectedHolder: View): Boolean {
return mCurrentSelectedName == selectedName && mCurrentSelectedHolder == selectedHolder
}
private fun isDifferentSelection(selectedName: String, selectedHolder: View): Boolean {
return mCurrentSelectedName != selectedName && mCurrentSelectedHolder != selectedHolder
}
/**
* This shows the Highlight Background by animating its Alpha, X and Y.
* Animating Alpha property should run slowly.
* Animating X and Y properties should run very fast, in a blink.
*
* When animation starts, just make sure to update width and height of Highlight Background.
*/
private fun showHighlightBackground() {
val (newX, newY, newWidth, newHeight) = calculateNewPositionValues()
val alphaProperty = PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f)
val alphaAnimator = ObjectAnimator.ofPropertyValuesHolder(
mHighlightBackground,
alphaProperty
)
val xProperty = PropertyValuesHolder.ofFloat(View.X, newX)
val yProperty = PropertyValuesHolder.ofFloat(View.Y, newY)
val xyAnimator = ObjectAnimator.ofPropertyValuesHolder(
mHighlightBackground,
xProperty, yProperty
)
alphaAnimator.duration = FADE_DURATION
xyAnimator.duration = BLINK_DURATION
val animatorSet = AnimatorSet()
animatorSet.playSequentially(
xyAnimator,
alphaAnimator
)
animatorSet.addListener(object: AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
mHighlightBackground.layoutParams = ConstraintLayout.LayoutParams(newWidth, newHeight)
}
})
animatorSet.start()
}
/**
* This hides the Highlight Background by animating its Alpha.
* Animating Alpha property should run slowly.
*
* When animation ends, just make sure update current selections as required.
*/
private fun hideHighlightBackground(clearCurrentSelections: Boolean) {
val alphaProperty = PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f)
val alphaAnimator = ObjectAnimator.ofPropertyValuesHolder(
mHighlightBackground,
alphaProperty
)
alphaAnimator.duration = FADE_DURATION
alphaAnimator.addListener(object: AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
mCurrentSelectedName = if (clearCurrentSelections) null else mCurrentSelectedName
mCurrentSelectedHolder = if (clearCurrentSelections) null else mCurrentSelectedHolder
}
})
alphaAnimator.start()
}
/**
* This moves the Highlight Background by animating its X and Y.
* Animating X and Y properties is different here ..
* When alpha is 0:
* It means Highlight Background is off the screen, then animate quickly in a blink.
* when alpha is 1:
* Animate slowly.
*
* When animation starts:
* - Make sure to update width and height of Highlight Background.
* - Make sure to set alpha to 1
*/
private fun moveHighlightBackground() {
val (newX, newY, newWidth, newHeight) = calculateNewPositionValues()
val xProperty = PropertyValuesHolder.ofFloat(View.X, newX)
val yProperty = PropertyValuesHolder.ofFloat(View.Y, newY)
val xyAnimator = ObjectAnimator.ofPropertyValuesHolder(
mHighlightBackground, xProperty, yProperty
)
when (mHighlightBackground.alpha) {
// Highlight Background is off the screen, then animate quickly in a blink.
0f -> xyAnimator.duration = BLINK_DURATION
else -> {
xyAnimator.duration = SLOW_DURATION
xyAnimator.interpolator = FastOutSlowInInterpolator()
}
}
xyAnimator.addListener(object: AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
mHighlightBackground.layoutParams = ConstraintLayout.LayoutParams(newWidth, newHeight)
// Highlight Background is off the screen, then animate quickly in a blink.
if (mHighlightBackground.alpha == 0f) {
mHighlightBackground.alpha = 1f
}
}
})
xyAnimator.start()
}
/**
* This to calculate NewPosition values based on current selected NameHolder.
*/
private fun calculateNewPositionValues(): NewPosition {
val location = IntArray(2)
mCurrentSelectedHolder?.getLocationOnScreen(location)
val x = location[0].toFloat()
val y = location[1] - (mCurrentSelectedHolder?.height!! / 2f)
val width = mCurrentSelectedHolder?.width!!
val height = mCurrentSelectedHolder?.height!!
return NewPosition(x, y, width, height)
}
}
/**
* This is used for destructuring values from calculateNewPositionValues method
*/
data class NewPosition(val newX: Float, val newY: Float, val newWidth: Int, val newHeight: Int)
/**
* It should be implemented by NamesAdapter, so it can immediate scrolling is happening and move
* the Highlight Background accordingly
*/
interface OnRecyclerViewScrollCallback {
fun onScroll()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment