Last active
August 31, 2020 11:08
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This is used for destructuring values from calculateNewPositionValues method | |
*/ | |
data class NewPosition(val newX: Float, val newY: Float, val newWidth: Int, val newHeight: Int) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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