I have just migrated to the Fresco library for loading images in my app.
I need to listen to Image Loading Events, of course I read this article in documentation Listening to download events
This is exactly what I need, but....
There few things that I don't like.
My goal is to hide View if it fails to download it from the net.
I cannot reference SimpleDraweeView from controller, even on callback method. I need to hide View, but it seems that I cannot get reference to it.
Each time I need to load image, I need to create object of controller using Builder, and this can cause performance issues when using this approach with list of a lot of items with images.
holder.simpleDraweeViewImage.setController(Fresco.newDraweeControllerBuilder()
.setControllerListener(controllerListener)
.setUri(currentItem.getImage())
.build());
I need to able to have reference to the SimpleDraweeView from controller, and in MVC pattern approach it seems okay if controller is aware about view.
Please suggest the best way to rich my goal.
Thanks.
Can hide on onFailure method:
ControllerListener listener = new BaseControllerListener<ImageInfo>() {
#Override
public void onFinalImageSet(String id, #Nullable ImageInfo imageInfo, #Nullable Animatable animatable) {
//Action on final image load
}
#Override
public void onFailure(String id, Throwable throwable) {
//Action on failure
}
};
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setUri(uri)
.setControllerListener(listener)
.build();
draweeView.setController(controller);
Regarding 1, perhaps you can do something like this:
class ControllerListenerWithView() extends BaseControllerListener {
private final WeakReference<View> mViewReference;
ControllerListenerWithView(View view) {
mViewReference = new WeakReference<>(view);
}
#Nullable
protected View getView() {
return mViewReference.get();
}
}
Then:
ControllerListener controllerListener = new ControllerListenerWithView(holder.simpleDraweeViewImage) {
#Override
public void onFailure(String id, Throwable throwable) {
View view = getView();
if (view != null) {
view.setVisibility(View.GONE);
}
}
};
If you don't have the view accessible at the listener creation time, instead of passing view via listener constructor, you can add a setter method and do:
controllerListener.setView(holder.simpleDraweeViewImage);
controller = ...
holder.simpleDraweeViewImage.setController(controller);
If this looks ugly to you, well, that's because it is ugly :) Design that involves circular references is just ugly. DraweeController doesn't have a reference to the view (not directly at least). DraweeController references a DraweeHierarchy which references Drawables and the top-level drawable has a WeakReference to the parent view in order to propagate Drawable.Callback events. But that's it. DraweeController doesn't need view and we can't/won't keep reference to the view in it. The reason for that is that DraweeControllers and DraweeHierarchies can be used in contexts other than View and it is unnecessary for controller to have a back reference to the view. DraweeController controls DraweeHierarchy, not the view.
Regarding 2, while building controller, you can specify setOldController(view.getController()). That way the old controller which you are replacing will be reused while building a new one. This saves allocations and helps scroll-perf.
Related
I am creating a custom View and I would like to listen for the transformation changes. For example, the ones triggered by View#setScaleX. One way to do it is overriding all the methods:
setTranslationX
setTranslationY
setTranslationZ
setElevation
setRotation
setRotationX
setRotationY
setScaleX
setScaleY
setPivotX
setPivotY
setCameraDistance
setAnimationMatrix
Am I missing anything? I don't care for the top/left/bottom/right properties so they are left out intentionally. However this is cumbersome. It would be better if I can just get a callback and listen for it. Is that possible?
//Make some kind of callback
public interface TransformationCallback{
//String whatWho is an example it can be anything.
void onTransform(String whatWho);
}
public class YourView extends View{
private TransformationCallback callback;
//Pass an interface into the View constructor
public YourView(Context context, TransformationCallback callback){
super(context);
this.callback = callback;
}
}
#Override
public void setTranslationX(float x){
//call onTransform from the callback
callback.onTransform("setTranslationX was called");
super.setTranslationX(x);
}
The only problem with this, is it will not detect internal changes to the underlining values that these functions "set".
For example there is a variable inside View called protected int mLeft; Which is modified multiple times, internally not using functions.
The variable is also protected meaning abstractions of View can also modify it without function calls.
For the most part only external classes that mess with Views will use those functions which may or may not effect you.
(First of all: no, it's not a duplicate of mentioned question :P read, then push the button.)
I'm using the BottomNavigationView in one of my apps, loading Fragments with lists, which get their data from a ViewModel/LiveData/Dao. When selecting a Fragment via the BNV, it seems its animation somehow fights for UI-Thread time with the Fragment loading, causing it only to finish completely after the lists are displayed - which confuses me. I was under the impression, that LiveData calls are being handled async by default?
Stuttering gif
Is this a known thing?
ViewModel
public class ScheduleViewModel extends ViewModel {
private final LiveData<List<ScheduleInfo>> arrivals;
private final LiveData<List<ScheduleInfo>> departures;
public ScheduleViewModel() {
arrivals = SigmoDb.schedule().getArrivals();
departures = SigmoDb.schedule().getDepartures();
}
public LiveData<List<ScheduleInfo>> getArrivals() {
return arrivals;
}
public LiveData<List<ScheduleInfo>> getDepartures() {
return departures;
}
}
Fragment
public class ArrivalsFragment extends MainFragment {
private ScheduleDetailsAdapter adapter;
private ScheduleViewModel viewModel;
private final Observer<List<ScheduleInfo>> arrivalsObserver = new Observer<List<ScheduleInfo>>() {
#Override
public void onChanged(#Nullable List<ScheduleInfo> infoList) {
adapter.setData(infoList);
}
};
public static ArrivalsFragment newInstance() {
return new ArrivalsFragment();
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
adapter = new ScheduleDetailsAdapter(getActivity());
// using the parent fragment as LifeCycleOwner, since both its
// child Fragments use the same ViewModel
Fragment parent = getParentFragment();
if (parent == null) {
parent = this;
}
viewModel = ViewModelProviders.of(parent).get(ScheduleViewModel.class);
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
reObserveViewModel();
}
// remove and re-add observer until Google fixes the multiple observer issue
// TODO: remove when Google fixes the issue
// https://github.com/googlesamples/android-architecture-components/issues/47
private void reObserveViewModel() {
viewModel.getArrivals().removeObservers(this);
viewModel.getArrivals().observe(this, arrivalsObserver);
}
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_arrivals_departures, container, false);
RecyclerView recyclerView = view.findViewById(R.id.rv_schedule_details);
LinearLayoutManager llm = new LinearLayoutManager(this.getContext());
recyclerView.setLayoutManager(llm);
recyclerView.setAdapter(adapter);
return view;
}
}
For info: I timestamped the ViewModel's constructor start and end (to rule out those calls somehow being on the UI thread - takes 1 millisecond).
Narrowed down the issue
After Robin Davies' answer, I tried the Android Profiler and although I get some GC events now and then, I don't get them all the time with the stuttering being there every single time. However, delaying setting of the adapter data in the observer by 100ms seems to let the BNV animation complete when switching to the ArrivalsFragment:
No stuttering gif
All I did was changing
private final Observer<List<ScheduleInfo>> arrivalsObserver = new Observer<List<ScheduleInfo>>() {
#Override
public void onChanged(#Nullable List<ScheduleInfo> infoList) {
adapter.setData(infoList);
}
};
to
private final Observer<List<ScheduleInfo>> arrivalsObserver = new Observer<List<ScheduleInfo>>() {
#Override
public void onChanged(#Nullable final List<ScheduleInfo> infoList) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
adapter.setData(infoList);
}
}, 100);
}
};
So it seems that this part of your answer
Also if you post list results back to the foreground thread and
populate the adapter while the animation is running, that will force a
layout pass that will interfere with animation.
is the one I was struggling with in my particular case. While I'm a bit disappointed having to revert to using delays to make the animation fluent, I'm happy to have found the culprit and thank you very much for your help :)
Yes this is common enough problem.
Assuming that you've already moved heavy processing onto an background threads...
If you are doing really heavy lifting on the background thread, you can trigger garbage collects, which can block the foreground thread long enough to cause stuttering. Also if you post list results back to the foreground thread and populate the adapter while the animation is running, that will force a layout pass that will interfere with animation.
Try using the CPU usage/profiling tools to see what exactly is holding up the foreground thread.
Solutions to consider would be to postpone population of the fragments until the animation is finished. Or pre-populate the fragment. Or maybe block the background thread while animation is running (perhaps). Or postpone the animation until the fragment is populated and laid out (which gets potentially unpleasant). If the problem isn't caused by a garbage collect, you could delay creation/population of the adapter until the animation is finished.
I have a simple recyclerview that contains a custom ImageGLSurfaceView in which it shows a bitmap. If i had a simple imageview i would have used (Picasso or glide etc) for this easily. But i have ImageGLSurfaceView so i am trying to do is to mimic ImageLoader lib class. So i have ImageLoader class that has loadImage method which i call from recycler view's adapter and i cache/store ImageGLSurfaceView object in hashmap and handle it like below:
public void loadImage(final FilteredImage filteredImage, final ImageGLSurfaceView imageView) {
if(images.containsKey(filteredImage)) {
images.get(filteredImage).setImageBitmap(filteredImage.getBitmap());
images.get(filteredImage).setFilterWithConfig(filteredImage.getFilter());
} else {
images.put(filteredImage, imageView);
imageView.setSurfaceCreatedCallback(new ImageGLSurfaceView.OnSurfaceCreatedCallback() {
#Override
public void surfaceCreated() {
imageView.setImageBitmap(filteredImage.getBitmap());
imageView.setFilterWithConfig(filteredImage.getFilter());
}
});
}
}
But list is slow or i think it is recreating ImageGLSurfaceView object again on list scroll. Or maybe i should put surfaceCreatedCallback on background thread. So i decided to use RxJava for this purpose. I want to subscribe for ImageGLSurfaceView to be created and once it is created then set Image bitmap and dont create ImageGLSurfaceView again. Please guide me if this is the right approach to achieve this? Or should i completely change the code?
According to this https://github.com/MvvmCross/MvvmCross/wiki/View-Model-Lifecycle, we should implement save state and reload state to handle tombstoning.
But what happens if we have a value parameter in the Init method ?
example:
public StockDetailViewModel
{
int stockId;
...
void Init(int stockIdAsIntent)
{
stockId = stockIdAsIntent
}
protected override void ReloadFromBundle(IMvxBundle state)
{
id = int.Parse(state.Data["id"]);
}
protected override void SaveStateToBundle(IMvxBundle bundle)
{
bundle.Data["id"] = stockId;
}
async void Start()
{
stockModel = StockService.Get(stockId);
}
...
}
Now let say we navigate to a child view model called StockFormViewModel.
Doing this the SaveState method is called.
Now we open the Form, do our business, then call this.Close(this);
If we follow CIRS, first the Init will be called, and then the ReloadState.
But the Init method will fail because we are coming from a child view model, and no intent was given as argument, giving no chance to the ReloadState method to kick in and restore the stockId.
I'm pretty sure I'm doing something wrong here, but I cannot put my finger on it...
It turns out that I was mixing MVVMCross navigation with parent intent navigation (due to old refactoring of base class).
It makes perfectly sense that these won't work together.
I switch to full MVVMCross navigation, and with this, there are two scenarios:
If the view model is still in cache (mvvmcross has a VM cache), it gets the VM from there, and so, when you call close on the "child", none of the initialization methods are called (Init, Reload or Start).
If the view model is not in the cache, it will reconstruct the VM and call ReloadState instead of Init.
Im having big troubles using a Target inside an adapter. Im confused about the documentation on the code
Objects implementing this class must have a working implementation of
{#link #equals(Object)} and {#link #hashCode()} for proper storage internally. Instances of this
interface will also be compared to determine if view recycling is occurring. It is recommended
that you add this interface directly on to a custom view type when using in an adapter to ensure
correct recycling behavior.
Im trying to use the Target in this way:
class CustomTarget implements Target {
private ImageView imageView;
public CustomTarget(ImageView imageView) {
this.imageView = imageView;
}
#Override
public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom from) {
imageView.setImageDrawable(new RoundedAvatarDrawable(bitmap));
}
#Override
public void onBitmapFailed(Drawable errorDrawable) {
imageView.setImageDrawable(errorDrawable);
}
#Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
imageView.setImageDrawable(placeHolderDrawable);
}
#Override
public boolean equals(Object o) {
return imageView.equals(o);
}
#Override
public int hashCode() {
return imageView.hashCode();
}
}
#Override
public View getView(int position, View v, ViewGroup parent) {
....
RoundedAvatarDrawable r = new RoundedAvatarDrawable(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_avatar_seahorse));
ImageCacheController.with(mContext).getPicasso().load(member.getPicture_url()).resize(100, 100).centerCrop().placeholder(r).error(r).into(new CustomTarget(viewHolder.ivAvatar));
....
}
It's doesn't work and the images change between each others randomly
You don't show your whole getView function, so without knowing how you use the viewHandler, here's my take on what's going on:
Your problem is that you're creating a new CustomTarget every time getView gets called. You are going against the point of having a Target object. Let me elaborate.
When a new download request is made, previous requests to the same target get stopped or don't result in a call to the Target's callbacks. (so if the Target gets reused for a different row in a list it doesn't get both rows' images).
You are using a new object for each request, effectively hinting Picasso that each request is for a different row so to speak. The doc says "Instances of this interface will also be compared to determine if view recycling is occurring", so since each request has a newly created CustomTarget object, no two requests will have the same object and a row recycle won't be detected.
You're also using viewHolder. In this case I think the viewHolder should be extending the Target interface (if you only have 1 image per row). This way everytime you request a download you can use the same object and not create a new one.
You're also delegating the implementation of your CustomTarget to the ImageView's implementation. Make sure that ImageView's equals and hashCode functions fullfill the requirements Picasso asks for.
Some info on how to implement equals and hashCode: What issues should be considered when overriding equals and hashCode in Java?
It seems your equals method is broken. You are comparing an imageview to a custom target. This might fix it:
public boolean equals(Object o) {
if(o instanceof CustomTarget) {
return ((CustomTarget) o).imageView.equals(this.imageView);
}
return super.equals(o);
}