Parallax header effect with RecyclerView - android

I want to change my ListView I currently have over to use RecyclerView so I can make use of StaggeredGridLayoutManager but RecyclerView does not have the ability to add a header like ListView.
Usually with a ListView I set an empty view in the header and put the image below the listview and translate the bottom image with the scrolling of the list to create the Parallax effect.
So with out a header how can I create the same parallax effect with RecyclerView?

the easiest way to do it, is using below onScrollListener without relying on any library.
View view = recyclerView.getChildAt(0);
if(view != null && recyclerView.getChildAdapterPosition(view) == 0) {
view.setTranslationY(-view.getTop() / 2);// or use view.animate().translateY();
}
make sure your second viewHolder item has a background color to match the drawer/activity background. so the scrolling looks parallax.

So today I tried to archive that effect on a RecyclerView. I was able to do it but since the code is too much I will paste here my github project and I will explain some of the key points of the project.
https://github.com/kanytu/android-parallax-recyclerview
First we need to look at getItemViewType on the RecyclerView.Adapter class. This methods defines what type of view we're dealing with. That type will be passed on to onCreateViewHolder and there we can inflate different views. So what I did was: check if the position is the first one. If so then inflate the header, if not inflate a normal row.
I've added also a CustomRelativeLayout that clips the view so we don't have any trouble with the dividers and with the rows getting on top of the header.
From this point you seem to know the rest of the logic behind it.
The final result was:
EDIT:
If you need to insert something in adapter make sure you notify the correct position by adding 1 in the notifyItemChanged/Inserted method. For example:
public void addItem(String item, int position) {
mData.add(position, item);
notifyItemInserted(position + 1); //we have to add 1 to the notification position since we don't want to mess with the header
}
Another important edit I've done is the scroll logic. The mCurrentOffset system I was using didn't work with the item insertion since the offset will change if you add an item. So what I did was:
ViewHolder holder = findViewHolderForPosition(0);
if (holder != null)
((ParallaxRecyclerAdapter) getAdapter()).translateHeader(-holder.itemView.getTop() * 0.5f);
To test this I added a postDelayed Runnable, started the app, scrolled to the end, add the item in position 0, and scroll up again. The result was:
If anyone is looking for other parallax effects they can check my other repo:
https://github.com/kanytu/android-parallax-listview

For kotlin, you may config the recycler view as below
//setting parallax effects
recyclerView.addOnScrollListener(object :RecyclerView.OnScrollListener(){
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val view = recyclerView?.getChildAt(0)
if (view != null && recyclerView?.getChildAdapterPosition(view) === 0) {
val imageView = view.findViewById(R.id.parallaxImage)
imageView.translationY = -view.top / 2f
}
}
})

This answer is for those curious about adding a parallax header to a GridLayoutManager or a StaggeredGridLayoutManager
You'll want to add the following code to your adapter in either onBindViewHolder or onCreateViewHolder
StaggeredGridLayoutManager.LayoutParams layoutParams =
(StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
layoutParams.setFullSpan(true);

Related

RecyclerView - Continuous Columns Layout

I am trying to create a layout where items would follow one another in columns (see image below) but I am not getting there yet. I have tried GridLayoutManager and StaggeredGridLayoutManager - the problem with both neither provides the feature of item flowing into another column and following each other this way. With my current attempt I am trying FlexboxLayoutManager but the result I am getting is always columns with single items instead of the items flowing one after another.
The desired behavior is that the items are located one after another and when the high of the recycler doesn't allow for the full item view it should be broken down to the next column.
Here is what I am trying right now:
mBinding?.activeRecycler?.layoutManager = FlexboxLayoutManager(context).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
alignItems = AlignItems.STRETCH
}
And this is getting me one item per column.
Trying to achieve this:
I highly doubt this is possible.
The RecyclerView, its adapters and its layout managers all are not designed to alter the fundamental form of a view.
Meaning that "splitting" one would not be possible.
The RecyclerView is designed to understand how many views are in sight at the same time, create that many views only and then bind the underlying objects to the views respectively.
Meaning the RecyclerView doesn't "Cut a View in half and displays its halves in different places".
The only way in which a constellation like yours would be possible, was if the layout manager is specifically designed to display one item in multiple views and thereby multiple positions. Which would then allow it to be displayed as you described. However, as I said, that would mean the view 3 in the middle and the view 3 in the last column would be two views being bound to the same object or a copy of it. (Or someone went completely crazy and actually split the view, which I doubt).
I don't believe that any of the standard layout managers are capable of it and I doubt that you can even achieve this without also altering the adapter accordingly, at the very least. Because the adapter basically does the binding so without its help the standard layout managers wouldn't be able to do the double binding as described above.
That being said, this is just a very good guess, going by the principles of the view and its components. I have not read the source code or full description of every layout manager.
The way I understand your problem is like this: You have your current list of data that contains the text fields and you want to show them on the normal way, one list item one view item in recycler view.
But based on your design requirements this is not possible.
My idea to achieve that is like this:
You have to create a new list which will separate one item of the previous list into 2,3 or more items to fit in your columns.
private fun demo() {
val originalList = listOf<String>()
val newScreenSpecificList = mutableListOf<String>()
val columnHeight = 3//example number of lines
val columnWidth = 10//example number of chars
var columnsIndex = 0//index of column
var currentColumnHeight = 0 // current column filled height
originalList.forEach {
if (currentColumnHeight + getTextHeight(it, columnWidth) <= columnHeight) {
newScreenSpecificList.add(it)
currentColumnHeight = currentColumnHeight + getTextHeight(it, columnWidth)
} else {
//here is the part where your text is bigger then your column height so you need to divide it
val textForSpaceLeft = getTextForSpaceLeft(it, columnHeight - currentColumnHeight)
newScreenSpecificList.add(textForSpaceLeft)
currentColumnHeight = currentColumnHeight + getTextHeight(textForSpaceLeft, columnWidth)
if (currentColumnHeight >= columnHeight) {
columnsIndex++
}
if (getTextForNewSpaceLeft(it, columnHeight - currentColumnHeight)){
//continue to repeat logic for new column
//...
}
}
if (currentColumnHeight >= columnHeight) {
columnsIndex++
}
}
}
private fun getTextForSpaceLeft(it: String, spaceLeft: Int): String {
return "it"// return text for the available space
}
private fun getTextForNewSpaceLeft(it: String, spaceLeft: Int): String {
return "new column also"// return text left for the new available space
}
private fun getTextHeight(text: String, columnWidth: Int): Int {
return 2//todo your logic to convert text length to number of lines needed for a specific width of the column
}
Now you need to continue this logic it is not complete, I hope it helps you.
I guess your problem is with the LayoutParams of items which are being created in your adapter. probably the height is set to match_parent in items. You can try to change the LayoutParams of itemViews in your adapter's onCreateViewHolder/onBindViewHolder. Or if the items' heights are kinda tricky to calculate, you can create a customView and try calculate the height in onMeasure and set the height to wrap_content
try to set items' height to wrap_content or if you want to do it in code, something like this:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlexItemViewHolder {
val infatedView = ...
infatedView.layoutParams = FlexboxLayoutManager.LayoutParams(FlexboxLayoutManager.LayoutParams.WRAP_CONTENT, FlexboxLayoutManager.LayoutParams.WRAP_CONTENT)
infatedView.addView(textView)
return FlexItemViewHolder(f)
}

How to animate only selected items in RecyclerView

I have RecyclerView and I want to animate one View inside RecyclerView item only for items which meet certain condition.
I'm using ObjectAnimator for this. Sometimes it is working fine, but sometimes animation is applied to all views. I assume there is some recycling problem with Views, but I'm not certain what can cause that.
Example:
val halfHeightCropped = halfHeight - (stopIndicatorHeight/2) //half item height minus half timeline circle height
val halfDistCurrToNext = timeDistCurrToNext/2
if (alreadyTraveled > halfDistCurrToNext){
animator = ObjectAnimator.ofFloat(indicator, "translationY", halfHeightCropped.toFloat()).apply {
duration = 0
start()
}
}
I'm an animating circle that is moving vertically through a timeline based on real-time. This circle is part of each RecyclerView item and in some items, this item is stationarily centered in item View, and some (1 at the time) is animating this circle vertically.
The example above is part of the function which is called from onBindViewHolder if the item meets the condition, otherwise, the view is reset to a default position. (layout param center vertical in an item).
when item select from recyclerView select that into adapter like
yourAdapter = new yourAdapter(new ArrayLis(),new ItemOnClick(){
#Override
public void onClick(int position){
yourAdapter.selectItem(position)
}
});
perform animate in bind() method of holder class.
if(selectedItem = getAdapterPosition){
//perform your animation for the selected item
}
your adapter should have the selectedItem variable and setter method for that.

Why RecyclerView smoothScrollToPosition scrolled to 0 happens slowly?

I have RecyclerView and use it instead ViewPager with BottomNavigationView from support library. RecyclerView for nicely and comfort scroll have PagerSnapHelper.
And i faced with terrible and strange problem:
when my listener for BottomNavigationView catches in method onNavigationItemSelected new position i make this:
override fun onNavigationItemSelected(item: MenuItem): Boolean {
var newPos = -1
//little code for checked only a NEW position, the same values ignored
recycler.smoothScrollToPosition(currentPos) //values 0, 1 and 2
return true
}
when i smoothScrollToPosition with currentPos = 0 recycler scroll to First position (0) very slow (compared to other
). Time between call method smoothScrollToPosition and onScrollStateChanged (for recycler view scroll listener with parameter newState = RecyclerView.SCROLL_STATE_IDLE) with values 1 and 2 very small, and with value = 0 about a second (!)
Why is this happening and how can I fix it?
Use recyclerView.scrollToPosition(position); instead of recycler.smoothScrollToPosition(currentPos)
recyclerView.scrollToPosition(position); will not show the animation of scrolling down, the effect will be immediate.
You can also try this if you set linearLayoutManager for your recyclerView
linearLayoutManager.scrollToPositionWithOffset(position, 0);

Parallax effect on each item in a recycler view?

I'm trying to play with Parallax and getting some weird bugs, wondering if anyone can add some input to it. The only app I've seen implement parallax effectively is soundcloud. It's quite subtle, but each item has an image background and it has he parallax effect as you scroll.
I've created a custom RecyclerView to handle this, here is what I have so far:
public class ParallaxScrollListener extends RecyclerView.OnScrollListener {
private float scrollSpeed = 0.5f;
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int firstVisible = layoutManager.findFirstVisibleItemPosition();
int visibleCount = Math.abs(firstVisible - layoutManager.findLastVisibleItemPosition());
Matrix imageMatrix;
float tempSpeed = -100;
if (dy > 0) {
tempSpeed = scrollSpeed;
} else if (dy < 0) {
tempSpeed = -scrollSpeed;
}
for (int i = firstVisible; i < (firstVisible + visibleCount); i++) {
ImageView imageView = ((MyClass.MyAdapter.MyViewHolder) recyclerView.getLayoutManager().findViewByPosition(i).getTag()).image;
if (imageView != null) {
imageMatrix = imageView.getImageMatrix();
imageMatrix.postTranslate(0, tempSpeed);
imageView.setImageMatrix(imageMatrix);
imageView.invalidate();
}
}
}
In my RecyclerView Adapter's onBindView I have the following as well:
Matrix matrix = viewHolder.image.getImageMatrix();
matrix.postTranslate(0, 0);
viewHolder.image.setImageMatrix(matrix);
viewHolder.itemView.setTag(viewHolder);
Finally inside the onViewRecycled method I have the following:
#Override
public void onViewRecycled(MyViewHolder viewHolder) {
super.onViewRecycled(viewHolder);
if (viewHolder.image != null) {
viewHolder.image.setScaleType(ImageView.ScaleType.MATRIX);
Matrix matrix = viewHolder.image.getImageMatrix();
// this is set manually to show to the center
matrix.reset();
viewHolder.image.setImageMatrix(matrix);
}
}
I been working with this code on Github to get the idea
So the parallax works, but but views in my RecyclerView move as well. I have a CardView beneath the image and it moves, creating big gaps between each item. Scrolling is what causes this, the more the scroll up and down the bigger the gaps get, and the images get smaller as the parallax moves them out of their bounds.
I've tried messing with the numbers like scrollSpeed in the OnScrollListener but while it reduces the bug it also reduces the parallax.
Has anyone got any ideas on how I can achieve a bug free parallax effect on each item in my RecyclerView? I feel like I'm getting somewhere with this but it's still very buggy and I don't know what the next step is.
P.s I've tried looking at 3rd party libraries but they all seem to only use header parallax like the CoordinatorLayout, I haven't found any that do it just on each item in a list.
I'm hoping this question gets a good discussion going even if I don't solve my problem because Parallax seems to be underused in Android and there's very little around about it.
Thanks for you time, appreciate any help.
I managed to get Parallax working with this library: https://github.com/yayaa/ParallaxRecyclerView
For anyone doing this themselves, it's still a good thing to play and see how it works.
Similar concept to my code but it actually works! haha.
You are on the right track. You have to use a ScrollListener. Furtermore, you have to access RecyclerViews LayoutManager and iterate over all items that are visible and set translateY according to the amount of pixels scrolled.
The things get a little bit more complicated, because you can't use recyclerView.getChildAt(pos) because LayoutManager is responsible to layout elements and they might be in different order in the LayoutManager than getChildAt(pos).
So the algorithm basically should look like this (pseudo code, assuming LinearLayoutManager is used):
for (int i = layoutManager.findFirstVisibleItemPosition(); i <= layoutmanager.findLastVisibleItemPosition; i++){
// i is the adapter position
ViewHolder vh = recyclerView.findViewHolderForAdapterPosition(i);
vh.imageView.setTranslationY( computedParalaxOffset ); // assuming ViewHolder has a imageView field on which you want to apply the parallax effect
}

how to keep RecyclerView always scroll bottom

I Use Recyclerview Replace with list view
I want to keep Recyclerview always scroll bottom.
ListView can use this method setTranscriptMode(AbsListView.TRANSCRIPT_MODE_ALWAYS_SCROLL)
RecyclerView I use method smoothScrollToPosition(myAdapter.getItemCount() - 1)
but when Soft keyboard Pop ,its replace RecyclerView content.
If you want to keep the scroll position anchored to the bottom of the RecyclerView, it's useful in chat apps. just call setStackFromEnd(true) to on the LinearLayoutManager to make the keyboard keep the list items anchored on the bottom (the keyboard) and not the top.
This is because RV thinks its reference point is TOP and when keyboard comes up, RV's size is updated by the parent and RV keeps its reference point stable. (thus keeps the top position at the same location)
You can set LayoutManager#ReverseLayout to true in which case RV will layout items from the end of the adapter.
e.g. adapter position 0 is at the bottom, 1 is above it etc...
This will of course require you to reverse the order of your adapter.
I'm not sure but setting stack from end may also give you the same result w/o reordering your adapter.
recyclerView.scrollToPosition(getAdapter().getItemCount()-1);
I have faced the same problem and I solved it using the approach mentioned here. It is used to detect whether soft keyboard is open or not and if it is open, just call the smoothScrollToPosition() method.
A much simpler solution is to give your activity's root view a known ID, say '#+id/activityRoot', hook a GlobalLayoutListener into the ViewTreeObserver, and from there calculate the size diff between your activity's view root and the window size:
final View activityRootView = findViewById(R.id.activityRoot);
activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int heightDiff = activityRootView.getRootView().getHeight() - activityRootView.getHeight();
if (heightDiff > 100) {
recyclerView.smoothScrollToPosition(myAdapter.getItemCount() - 1);
}
}
});
Easy!
I have also faced same problem. But following code help me. I hope this is useful.
In this staus is arraylist.
recyclerView.scrollToPosition(staus.size()-1);
next one is:-
In This you can use adapter class
recyclerView.scrollToPosition(showAdapter.getItemCount()-1);
I ran into this problem myself and I ended up creating my own LayoutManager to solve it. It's a pretty straightforward solution that can be broken down into three steps:
Set stackFromEnd to true.
Determine whether forceTranscriptScroll should be set to true whenever onItemsChanged is called. Per the documentation, onItemsChanged gets called whenever the contents of the adapter changes. If transcriptMode is set to Disabled, forceTranscriptScroll will always be false, if it's set to AlwaysScroll, it will always be true, and if it's set to Normal, it will only be true if the last item in the adapter is completely visible.
In onLayoutCompleted, scroll to the last item in the list if forceTranscriptScroll is set to true and the last item in the list isn't already completely visible.
Below is the code that accomplishes these three steps:
import android.content.Context
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class TranscriptEnabledLinearLayoutManager(context: Context, transcriptMode: TranscriptMode = TranscriptMode.Normal) :
LinearLayoutManager(context) {
enum class TranscriptMode {
Disabled, Normal, AlwaysScroll
}
private var transcriptMode: TranscriptMode = TranscriptMode.Disabled
set(value) {
field = value
// Step 1
stackFromEnd = field != TranscriptMode.Disabled
}
private var forceTranscriptScroll = false
init {
this.transcriptMode = transcriptMode
}
// Step 2
override fun onItemsChanged(recyclerView: RecyclerView) {
super.onItemsChanged(recyclerView)
forceTranscriptScroll = when (transcriptMode) {
TranscriptMode.Disabled -> false
TranscriptMode.Normal -> {
findLastCompletelyVisibleItemPosition() == itemCount - 1
}
TranscriptMode.AlwaysScroll -> true
}
}
// Step 3
override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state)
val recyclerViewState = state ?: return
if (!recyclerViewState.isPreLayout && forceTranscriptScroll) {
// gets the position of the last item in the list. returns if list is empty
val lastAdapterItemPosition = recyclerViewState.itemCount.takeIf { it > 0 }
?.minus(1) ?: return
val lastCompletelyVisibleItem = findLastCompletelyVisibleItemPosition()
if (lastCompletelyVisibleItem != lastAdapterItemPosition ||
recyclerViewState.targetScrollPosition != lastAdapterItemPosition) {
scrollToPositionWithOffset(lastAdapterItemPosition, 0)
}
forceTranscriptScroll = false
}
}
}

Categories

Resources