I am doing a shared element transition from a fragment with a RecyclerView to a detail view, where an image is shared between the two. The transition is mostly fine, but because the two images are different sizes, when I click on one, it scales the image to the final size, and then animates the ImageView bounds as it should. This results in the image not matching the size of the bounds during the animation.
Here is a video of what I'm describing
DetailFragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
ViewCompat.setTransitionName(binding.podcastImage, "podcastImage_${args.podcastId}")
postponeEnterTransition()
}
// called from onStart
private fun observeViewModel() {
viewModel.podcastObservable.observe(this, Observer { podcast ->
Glide.with(requireContext())
.load(podcast.image)
.into(binding.podcastImage)
// other stuff
startPostponedEnterTransition()
})
}
GridFragment:
fun navigateToPodcastDetailFragment(podcastId: String) {
val args = Bundle()
args.putString("podcast_id", podcastId)
val directions = TopPodcastsFragmentDirections.viewPodcastDetails(podcastId)
val extras = FragmentNavigatorExtras(
podcast_image to "podcastImage_$podcastId"
)
Navigation.findNavController(requireActivity(), R.id.nav_host_fragment)
.navigate(directions, extras)
}
GridAdapter:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
getItem(position).let { podcast ->
with(holder) {
Glide.with(holder.itemView.context)
.load(podcastList[position].image)
.dontAnimate()
.dontTransform()
.into(binding.thumbnail)
bind(createOnClickListener(binding, podcast.id), podcast)
}
}
}
ViewHolder.bind() {
// other stuff
ViewCompat.setTransitionName(binding.thumbnail, "podcast${value.id}")
}
Grid item ImageView:
<ImageView
android:id="#+id/thumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:transitionName="#string/podcastImageTransition"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="#drawable/ic_launcher_background" />
DetailFragment ImageView:
<ImageView
android:id="#+id/podcast_image"
android:layout_width="164dp"
android:layout_height="164dp"
android:scaleType="centerCrop"
android:layout_marginTop="24dp"
android:transitionName="#string/podcastImageTransition"
app:layout_constraintTop_toBottomOf="#id/toolbar"
app:layout_constraintStart_toStartOf="parent"
tools:src="#drawable/ic_launcher_background"/>
I think I have the transition part mostly correct, but something about the way the image is being cropped or scaled isn't right during the transition.
Thanks in advance.
Make sure in DetailFragment to include .dontTransform() within Glide
// called from onStart
private fun observeViewModel() {
viewModel.podcastObservable.observe(this, Observer { podcast ->
Glide.with(requireContext())
.load(podcast.image)
.dontTransform()
.into(binding.podcastImage)
// other stuff
startPostponedEnterTransition()
})
}
Also, I've been working on this all day (for an app I'm making coincidentally), and I just found something else you can try. Try removing the image scale type in the xml files.
Related
We are having hard times to smoothly resize a here SDK map on Android.
We want to smoothly resize the map to the bottom sheet collapse and hidden state as shown in
But as you can see it does not really resize instead its jumps to the new position while the map keeps its dimensions and does not scale.
And this is what we did:
...
<com.here.sdk.mapview.MapView
android:id="#+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="#dimen/nine_grid_unit" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/menuBottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/white"
android:clickable="true"
android:elevation="#dimen/four_grid_unit"
android:focusable="true"
app:behavior_hideable="true"
app:behavior_peekHeight="#dimen/thirtytwo_grid_unit"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<View
android:id="#+id/tap_stop"
android:layout_width="#dimen/nine_grid_unit"
android:layout_height="#dimen/one_grid_unit"
android:layout_marginTop="#dimen/one_grid_unit"
android:background="#color/grey_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<edeka.digital.app.widget.SegmentedControlView
android:id="#+id/tabSwitchSegmentedControl"
android:layout_width="#dimen/thirtyfive_grid_unit"
android:layout_height="wrap_content"
android:paddingStart="#dimen/three_grid_unit"
android:paddingEnd="#dimen/three_grid_unit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/tap_stop"
app:segmentCount="2"
app:segmentTitles="#array/segment_titles_shop_search" />
</androidx.constraintlayout.widget.ConstraintLayout>
...
And code:
val bottomBehavior = BottomSheetBehavior.from(binding.menuBottomSheet)
bottomBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
val mapView = binding.map
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
bottomSheetBehaviorObservable.onNext(newState)
when (newState) {
BottomSheetBehavior.STATE_COLLAPSED -> {
mapView.bottom = binding.menuBottomSheet.top
mapView.invalidate()
}
BottomSheetBehavior.STATE_HIDDEN -> {
mapView.bottom = binding.menuBottomSheet.top
mapView.invalidate()
}
else -> { /* void */
}
}
}
})
I would have expected some kind of resize() function or that it layouts itself if layout dimensions change.
What we really want is already implemented in HERE WeGo App. The whole maps scales (inc. here logo) if user swipes the bottom sheet:
Can anyone help us out?
The demo shown in 1 can be found here:
https://github.com/edekadigital/heremaps-demo
The best solution that I've found to achieve it is to add a new method:
private fun updateMapView(bottomSheetTop: Int) {
val mapView = binding.map
val principalY = Math.min(bottomSheetTop / 2.0, mapView.height / 2.0)
mapView.camera.principalPoint = Point2D(mapView.width / 2.0, principalY)
val logoMargin = Math.max(0, mapView.bottom - bottomSheetTop)
mapView.setWatermarkPosition(WatermarkPlacement.BOTTOM_CENTER, logoMargin.toLong())
}
and call it in onSlide and onStateChanged like this:
updateMapView(bottomSheet.top)
Note that you need to have the HERE logo at the bottom center position, otherwise it can't use an adjustable margin.
I was also trying to resize the map view, but the results were unsatisfying. Here is the code if you want to give a try:
private fun updateMapView(bottomSheetTop: Int) {
val mapView = binding.map
mapView.layoutParams.height = bottomSheetTop
mapView.requestLayout()
}
It looks like that your map view is covered by the sliding panel and is not redrawn during slide animation. It renders only when the state changes. You can try to add mapView.invalidate() in onSlide method, like this:
override fun onSlide(bottomSheet: View, slideOffset: Float) {
mapView.invalidate()
}
However, to be sure if that's the actual reason, I would need to get and build your code.
I was able to get your code, compile and reproduce the bug. I've found two options to fix that, both tested on an emulator and a real device.
Copy the code from state change handling code into onSlide method:
override fun onSlide(bottomSheet: View, slideOffset: Float) {
mapView.bottom = binding.menuBottomSheet.top
mapView.invalidate()
}
Remove map view resizing and invalidating code at all. It basically makes the whole setupBottomSheet method redundant. Map view works correctly without resizing and it's a preferable way to fix it, as it involves less code and operations.
I have a RecyclerView that loads images from URLs using Glide. Now the URLs are retrieved from Firebase using pagination as you can see below. The issue is that when the MainActivity (which contains the below code and the recyclerview) is first initialized there is a substantial lag in the UI (very laggy and choppy scrolling, options menu takes 3 seconds to open etc.) and the images take a while to load. After i scroll down though and reach the end of the RecyclerView for the first page of data, the OnScrollListener is triggered and i start loading new data from a new query. I've tried my best to optimize what Glide does based on suggestions from a user on another post i made and i also set the adapter.setHasFixedSize to true without luck. Any idea what's happening here? Am i hanging the UI thread somehow despite the queries being async?
EDIT
: Could Glide be causing the lag on the main Thread due to it having to load multiple images into the recycler view's imageViews? And if so, what can i do to counter that?
Here's how i handle the pagination of the data i get from Firebase and notify the adapter:
class MainActivity : AppCompatActivity() {
private val TAG: String = MainActivity::class.java.simpleName // Tag used for debugging
private var queryLimit : Long = 50 // how many documents should the query request from firebase
private lateinit var iconsRCV : RecyclerView // card icons recycler view
private lateinit var lastVisible:DocumentSnapshot
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rootRef: FirebaseFirestore = FirebaseFirestore.getInstance()
val urlsRef : CollectionReference = rootRef.collection("CardIconUrls")
val query : Query = urlsRef.orderBy("resID",Query.Direction.ASCENDING).limit(queryLimit) // create a query for the first queryLimit documents in the urlsRef collection
// Setting Toolbar default settings
val toolbar : Toolbar = findViewById(R.id.mainToolbar)
setSupportActionBar(toolbar) // set the custom toolbar as the support action bar
supportActionBar?.setDisplayShowTitleEnabled(false) // remove the default action bar title
// RecyclerView initializations
iconsRCV = findViewById(R.id.cardIconsRCV)
iconsRCV.layoutManager = GridLayoutManager(this,5) // set the layout manager for the rcv
val iconUrls : ArrayList<String> = ArrayList() // initialize the data with an empty array list
val adapter = CardIconAdapter(this,iconUrls) // initialize the adapter for the recyclerview
iconsRCV.adapter = adapter // set the adapter
iconsRCV.setHasFixedSize(true)
iconsRCV.addOnScrollListener(object:OnScrollListener(){
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if(!iconsRCV.canScrollVertically(1) && (newState == RecyclerView.SCROLL_STATE_IDLE) && ((iconsRCV.layoutManager as GridLayoutManager).findLastVisibleItemPosition() == (iconsRCV.layoutManager as GridLayoutManager).itemCount-1)) {
Log.d(TAG,"End of rcv-Starting query")
val nextQuery = urlsRef.orderBy("resID",Query.Direction.ASCENDING).startAfter(lastVisible).limit(queryLimit).get().addOnCompleteListener { task ->
if(task.isSuccessful) {
Log.d(TAG,"Next query called")
for(document:DocumentSnapshot in task.result!!) {
iconUrls.add(document.get("url").toString())
}
lastVisible = task.result!!.documents[task.result!!.size()-1]
adapter.notifyDataSetChanged()
}
}
}
}
})
query.get().addOnCompleteListener {task: Task<QuerySnapshot> ->
if(task.isSuccessful) {
Log.d(TAG,"Success")
for(document:DocumentSnapshot in task.result!!) {
Log.d(TAG,"Task size = " + task.result!!.size())
iconUrls.add(document.get("url").toString()) // add the url to the list
}
lastVisible = task.result!!.documents[task.result!!.size()-1]
adapter.notifyDataSetChanged() // notify the adapter about the new data
}
}
}
Here's the recyclerview adapter:
public class CardIconAdapter extends RecyclerView.Adapter<CardIconAdapter.ViewHolder> {
private List<String> urlsList;
private Context context;
class ViewHolder extends RecyclerView.ViewHolder {
ImageView iconImg;
ViewHolder(#NonNull View view) {
super(view);
iconImg = view.findViewById(R.id.cardIcon);
}
}
public CardIconAdapter(Context cntxt, List<String> data) {
context = cntxt;
urlsList = data;
}
#NonNull
#Override
public CardIconAdapter.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_icons_rcv_item,parent,false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull CardIconAdapter.ViewHolder holder, int position) {
RequestOptions requestOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL);
GlideApp.with(context).load(urlsList.get(position)).thumbnail(0.25f).centerCrop().dontTransform().apply(requestOptions).into(holder.iconImg);
}
#Override
public int getItemCount() {
return urlsList.size();
}
}
I finally figured out the issue after a lot of trial and error. My first mistake was not posting my xml layout file for the recyclerview item because that was the source of the performance issues. The second mistake was that I was using a LinearLayout and had set its own layout_width and layout_height attributes to 75dp instead of the ImageView's which is nested inside of the LinearLayout and was using wrap_content for the ImageView's respective attributes. So to fix the performance issues i did the following :
I changed the LinearLayout to a ConstraintLayout (i read that it is much more optimized in general)
I set the ConstraintLayout's layout_width & layout_height attributes to wrap_content and finally
I set the actual ImageView's layout_width & layout_height attributes to 75dp which is the actual size that i want the image to be
Here's the final layout file for each item of the recyclerview after the changes :
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:padding="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/cardIcon"
android:layout_width="75dp"
android:layout_height="75dp"
android:contentDescription="cardIcon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="#tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
Some of the ways to fix lagging when loading large number of images
Setting fixed size true to your RecyclerView but it wont help you if pagination involves
mRecyclerView.setHasFixedSize(true);
Overriding pixels of the image using glide like this
Glide.with(this)
.load(YOUR_URL)
.apply(new RequestOptions().override(dimenInPx, dimenInPx)
.placeholder(R.drawable.placeHolder).error(R.drawable.error_image))
.into(imageview);
Add hardwareAccelerated="true" property to your activity tag inside manifest like
<activity
...
android:hardwareAccelerated="true"
...
/>
I think this may help.
Try to use paging library combined with recyclerview
link reference:
Paging Library for Android With Kotlin: Creating Infinite Lists
Android Jetpack: manage infinite lists with RecyclerView and Paging (Google I/O '18)
This answer from Github was very helpful
Basically, if you set the exact dimensions of your image, it stops Glide from constantly having to figure out how big the image needs to be, and significantly improves performance. This helped take my RecyclerView from unusable to perfectly fine, just by setting a defined height and width on the ImageViews I was loading.
Trust me. Don't use ConstraintLayout as in the accepted answer. It's performance is low when compared to the LinearLayout. According to my measurements, it took average of 16 ms to inflate a layout file with ConstraintLayout and 10 ms for a layout file with LinearLayout. To have smooth scroll effect, it has to inflate layout at least within 16 ms.
In a Glide sample app here, they have created a SquareImageView which returns same width and height.
class SquareImageView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) :
AppCompatImageView(context, attrs, defStyleAttr) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
}
}
Don't forget to declare styleable in attrs.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.example.SquareImageView
android:id="#+id/image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="#dimen/padding_normal"
android:scaleType="centerCrop" />
<TextView
android:id="#+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="#dimen/padding_small"
android:textAlignment="center"
android:textColor="#color/secondaryTextColor"
android:textSize="15sp" />
</LinearLayout>
Try to avoid ConstraintLayout in the RecyclerView as it can cause some lag when inflating ViewHolders for the first time.
Another option will be to pre-inflate some views and use them in onCreateViewHolder
init {
val layoutInflater = LayoutInflater.from(context)
mPreInflated = List(20) {
MyLayoutBinding.inflate(layoutInflater, null, false)
}.iterator()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = if (mPreInflated.hasNext()) {
mPreInflated.next()
} else {
val layoutInflater = LayoutInflater.from(context)
MyLayoutBinding.inflate(layoutInflater, null, false)
}
return MyViewHolder(binding)
}
I'm trying to make a transition with simple animation of shared element between Fragments. In the first fragment I have elements in RecyclerView, in second - exactly the same element (defined in separate xml layout, in the list elements are also of this type) on top and details in the rest of the view. I'm giving various transitionNames for all elements in bindViewHolder and in onCreateView of target fragment I'm reading them and set them to element I want make transition. Anyway animation is not happening and I don't have any other ideas. Here below I'm putting my code snippets from source and target fragments and list adapter:
ListAdapter:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = list[position]
ViewCompat.setTransitionName(holder.view, item.id)
holder.view.setOnClickListener {
listener?.onItemSelected(item, holder.view)
}
...
}
interface interactionListener {
fun onItemSelected(item: ItemData, view: View)
}
ListFragment (Source):
override fun onItemSelected(item: ItemData, view: View) {
val action = ListFragmentDirections.itemDetailAction(item.id)
val extras = FragmentNavigatorExtras(view to view.transitionName)
val data = Bundle()
data.putString("itemId", item.id)
findNavController().navigate(action.actionId, data, null, extras)
}
SourceFragmentLayout:
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/pullToRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="#layout/item_overview_row" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
DetailFragment (Target):
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = inflater.inflate(R.layout.fragment_detail, container, false)
val itemId = ItemDetailFragmentArgs.fromBundle(arguments).itemId
(rootView.findViewById(R.id.includeDetails) as View).transitionName = itemId
sharedElementEnterTransition = ChangeBounds().apply {
duration = 750
}
sharedElementReturnTransition= ChangeBounds().apply {
duration = 750
}
return rootView
}
DetailFragmentLayout:
<include
android:id="#+id/includeDetails"
layout="#layout/item_overview_row"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
ItemOverviewRowLayout (this one included as item in recyclerView and in target fragment as header):
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:orientation="vertical" >
I made also another application using Jetpack navigation, shared elements and elements described by the same layout.xml and it's working since I'm not making transition from recyclerView to target fragment. Maybe I'm wrong here, setting the transitionName to found view in target fragment? I don't know how to make it another way, because the IDs of target included layout should be unique because of recyclerView items.
Okay, I found that how should it looks like to have enter animation with shared element:
In DetailFragment (Target) you should run postponeEnterTransition() on start onViewCreated (my code from onCreateView can be moved to onViewCreated). Now you have time to sign target view element with transitionName. After you end with loading data and view, you HAVE TO run startPostponedEnterTransition(). If you don't do it, ui would freeze, so you can't do time consuming operations between postponeEnterTransition and startPostponedEnterTransition.
Anyway, now the problem is with return transition. Because of course it's the same situation - you have to reload recyclerView before you release animation. Of course you can also use postponeEnterTransition (even if it's return transition). In my case, I have list wrapped by LiveData. In source fragment lifecycle observer is checking data. There is another challenge - how to determine if data is loaded. Theoretically with recyclerView you can use helpful inline function:
inline fun <T : View> T.afterMeasure(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
...and in code where you are applying your layout manager and adapter you can use it like this:
recyclerView.afterMeasure { startPostponedEnterTransition() }
it should do the work with determine time when return animation should start (you have to be sure if transitionNames are correct in recyclerView items so transition can have target view item)
From the answer that using ViewTreeObserver is quite consume resources a lot. and also have a lot of processes to do. so I do suggest you use doOnPreDraw instead of waiting after recyclerView was measured. the code implement will like this below.
recyclerView.doOnPreDraw {
startPostponedEnterTransition()
}
I currently have a RecyclerView containing posts (similar to facebook posts or tweets etc), with optional images. In the onBindViewHolder method of my adapter, I'm loading the image using Picasso but I won't know the size of the image until it has loaded. I want the post to appear normal (without an image) until the image has loaded, then the image should appear and the row height should increase to fit the new image with it's original aspect ratio.
At the moment when the image is loading, it's remaining attached to the bottom of the row while covering the top half of the row, even though there's a constraint from the top of the ImageView to the bottom of the main body TextView.
My item is using a ConstraintLayout as the root, the ImageView is defined as:
<ImageView
android:id="#+id/imageViewMain"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="#+id/textViewBody"
app:layout_constraintStart_toStartOf="#+id/textViewBody"
app:layout_constraintTop_toBottomOf="#+id/textViewBody" />
My onBindViewHolder method is similar to this (I'm using Kotlin):
override fun onBindViewHolder(holder: HomeAdapter.ViewHolder, position: Int) {
val item = items[position]
val mainImage = item.imageUrl
if (!mainImage.isNullOrEmpty()) {
Picasso.get().load(mainImage).into(holder.imageViewMain, object: Callback{
override fun onSuccess() {
// Could I do something here?
}
override fun onError(e: Exception?) { }
})
}
}
Has anyone got any tips on how to increase my row height when the image loads, rather than letting the image cover up the rest of the row?
Try this
//set visibility gone
//set height 250dp
// set scale_type = fitXY
<ImageView
android:id="#+id/imageViewMain"
android:layout_width="0dp"
android:layout_height="250dp"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:adjustViewBounds="true"
android:scaleType="fitXY"
android:visibility="Gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="#+id/textViewBody"
app:layout_constraintStart_toStartOf="#+id/textViewBody"
app:layout_constraintTop_toBottomOf="#+id/textViewBody" />
In your function
// set visibility Visible in onSuccess callback.
override fun onBindViewHolder(holder: HomeAdapter.ViewHolder, position: Int) {
val item = items[position]
val mainImage = item.imageUrl
if (!mainImage.isNullOrEmpty()) {
Picasso.get().load(mainImage).into(holder.imageViewMain, object: Callback{
override fun onSuccess() {
// Could I do something here?
holder.imageViewMain.setVisibility(View.Visible)
}
override fun onError(e: Exception?) { }
})
}
}
I have a recyclerview that loads a set of items which mainly display an image. I retrieve this items in the background, in batches of 100. I load the images using Picasso. Images are quite big, but I resize them using fit().
Whenever the screen is loaded or refreshed using SwipeRefreshLayout, the UI blocks for less than a second, but enough to be noticeable. If I dont load the images but put just the text, then the UI block does not happen.
I put logging lines on Picasso and on every refresh the 100 images are retrieved, but I would guess Picasso is working in a background thread?
Adapter:
#ActivityScope
class LimitableListAdapter #Inject constructor() : RecyclerView.Adapter<LimitableListAdapter.ViewHolder>() {
private var events: MutableList<Event> = mutableListOf()
private var itemClick: ((Event, View) -> Unit)? = null
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding : ItemVideoGridScoreBinding = holder.binding
var viewModel = binding.viewModel
val event = events[position]
//Unbind old viewModel if we have one
viewModel?.unbind()
// Create new ViewModel, set it, and bind it
viewModel = EventViewModel(event)
binding.viewModel = viewModel
viewModel.bind()
holder.setClickListener(itemClick)
}
override fun getItemCount(): Int = events.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = DataBindingUtil.inflate<ItemVideoGridScoreBinding>(
LayoutInflater.from(parent.context),
R.layout.item_video_grid_score,
parent,
false
)
return ViewHolder(binding)
}
fun updateEvents(events: List<Event>, stride: Int) {
var size = this.events.size
Timber.w("Updating with: " + events.joinToString(",", transform = { e -> e.id.toString() }))
this.events = events.toMutableList()
notifyDataSetChanged()
/*if (size == 0) {
Timber.w("branch 1")
var mutableList = events.toMutableList()
if(mutableList.size == 0)
return
mutableList.add(Event.mockEvent(stride))
this.events.addAll(mutableList)
notifyDataSetChanged()
} else {
if (size > 2) {
Timber.w("branch 2.1")
this.events.addAll(size - 1, events.toMutableList())
notifyItemRangeChanged(size-1, events.size)
}
else {
Timber.w("branch 2.2")
this.events.addAll(size, events.toMutableList())
notifyItemRangeChanged(size, events.size)
}
}*/
Timber.i("New list is: " + this.events.joinToString(",", transform = { e -> e.id.toString() }))
}
fun clearList(){
this.events.clear()
notifyDataSetChanged()
}
fun setClickListener(itemClick: ((Event, View) -> Unit)?) {
this.itemClick = itemClick
}
class ViewHolder(val binding: ItemVideoGridScoreBinding) : RecyclerView.ViewHolder(binding.root) {
fun setClickListener(callback: ((Event, View) -> Unit)?) {
binding.viewModel.clicks().subscribe() {
callback?.invoke(binding.viewModel.event, itemView)
}
}
}
}
BindingUtils:
#BindingAdapter({"app:coverUrl"})
public static void loadCover(ImageView view, String imageUrl) {
Picasso p = Picasso.with(view
.getContext());
p.setIndicatorsEnabled(true);
p.load(imageUrl)
.fit()
.centerInside()
.error(R.drawable.ic_broken_image)
.into(view);
}
}
xml:
(...)
<ImageView
android:id="#+id/event_cover"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:coverUrl="#{videoItem.cover}"
tools:src="#drawable/img"
/>
(...)
Assuming you are not able to create thumbnails on the server side (which would be the easiest solution), my suggestion would be to go with one of the following
Using a local drawable as a Placeholder. This will not block the UI thread and the image can load in the background. Behaviour will be similar to how Instagram behaves when you load the grid of images. Something like below.
p.load(imageurl).placeholder(R.drawable.localFile).fit().centerInside().error(R.drawable.ic_broken_image).into(view)
Use Glide. Glide does have powerful capability to create Thumbnails. And you can load the Thumbnails into the view much faster than the full images.
Resizing several big images takes some time. While Picasso does caching over the resized images, the first time it will still need to resize every one (and subsequent times it will just use the resized ones on the cache, thus taking less time).
Also, using fit can take more time than just using resize because it needs to calculate the size depending on the layout. If you can calculate the static size yourself (just once) and then use resize it should improve loading times.
But most of the times, it would be better if your server could send you smaller images/thumbnails when previewing, and then a full size image if you want to see it on some detail screen.
As you are loading 100 images and as you mentioned images are big,
so it will block the UI Thread for sure, if you want to avoid this so You can resize the images with Picasso:
Picasso.with(context)
.load(-yourImgURL-)
.resize(450, 200) // resizes image to desired dimensions here
.into(-yourImageView);