I am trying to create a "simple" image shared element transition between a recycler view item (Activity A) & a view pager (Activity B).
I have managed to smoothen everything but it seems that no matter how I set my exit/reenter/enter/return transitions the transition back from B to A, when the shared element is settled back, is flickering.
And by flickering I mean the entire screen.
I am using AppCompat themes & ActivityCompat methods with combinations of PreDrawListeners in both A (reenter) and B (when image is set using Glide).
Heres Activity A related code:
private Bundle mReenterState;
#TargetApi(Build.VERSION_CODES.LOLLIPOP) #Override protected void initActivityTransitions() {
super.initActivityTransitions();
getWindow().setSharedElementsUseOverlay(false);
setExitSharedElementCallback(mCallback);
}
private final SharedElementCallback mCallback = new SharedElementCallback() {
#TargetApi(Build.VERSION_CODES.LOLLIPOP) #Override public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
if (mReenterState != null) {
int startingSelection = mReenterState.getInt(DefinesGlobal.KEY_STARTING_INDEX);
int currentSelection = mReenterState.getInt(DefinesGlobal.KEY_CURRENT_INDEX);
if (startingSelection != currentSelection) {
String newTransitionName = mHotel.getDetail().getHotelImagesGallery().get(currentSelection);
View newSharedElement = mContentView.getGalleryView().getRecyclerViewPager()
.getLayoutManager().findViewByPosition(currentSelection);
if (newSharedElement != null) {
names.clear();
names.add(newTransitionName);
sharedElements.clear();
sharedElements.put(newTransitionName, newSharedElement);
}
}
mReenterState = null;
} else {
// If mReenterState is null, then the activity is exiting.
View navigationBar = findViewById(android.R.id.navigationBarBackground);
View statusBar = findViewById(android.R.id.statusBarBackground);
if (navigationBar != null) {
names.add(navigationBar.getTransitionName());
sharedElements.put(navigationBar.getTransitionName(), navigationBar);
}
if (statusBar != null) {
names.add(statusBar.getTransitionName());
sharedElements.put(statusBar.getTransitionName(), statusBar);
}
}
}
};
#Override public void onActivityReenter(int resultCode, Intent data) {
super.onActivityReenter(resultCode, data);
mReenterState = new Bundle(data.getExtras());
int startingSelection = mReenterState.getInt(DefinesGlobal.KEY_STARTING_INDEX);
int currentSelection = mReenterState.getInt(DefinesGlobal.KEY_CURRENT_INDEX);
if (startingSelection != currentSelection) {
mContentView.getGalleryView().getRecyclerViewPager().scrollToPosition(currentSelection);
}
ActivityCompat.postponeEnterTransition(this);
mContentView.getGalleryView().getRecyclerViewPager().getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
mContentView.getGalleryView().getRecyclerViewPager().getViewTreeObserver().removeOnPreDrawListener(this);
mContentView.getGalleryView().getRecyclerViewPager().requestLayout();
ActivityCompat.startPostponedEnterTransition(ViewModelHotelDetailsActivity.this);
return true;
}
});
}
Heres Activity B related code.
private boolean mIsReturning;
#Override public void finishAfterTransition() {
mIsReturning = true;
Intent data = new Intent();
data.putExtra(DefinesGlobal.KEY_CURRENT_INDEX, mCurrentSelection);
data.putExtra(DefinesGlobal.KEY_STARTING_INDEX, mStartingSelection);
setResult(RESULT_OK, data);
super.finishAfterTransition();
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP) #Override protected void initActivityTransitions() {
super.initActivityTransitions();
ActivityCompat.postponeEnterTransition(this);
setEnterSharedElementCallback(mCallback);
}
private final SharedElementCallback mCallback = new SharedElementCallback() {
#TargetApi(Build.VERSION_CODES.LOLLIPOP) #Override public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
if (mIsReturning) {
ViewModelFullSizeGalleryFragment fragment =
((FullSizeGalleryPagerAdapter) mPager.getAdapter()).getCurrent();
ImageView sharedElement = fragment.getImageView();
if (sharedElement == null) {
names.clear();
sharedElements.clear();
} else if (mStartingSelection != mCurrentSelection) {
names.clear();
names.add(sharedElement.getTransitionName());
sharedElements.clear();
sharedElements.put(sharedElement.getTransitionName(), sharedElement);
}
}
}
};
Activity B view pager fragment:
ublic class ViewModelFullSizeGalleryFragment extends ViewModelBaseFragment {
#Override protected int getLayoutId() {
return R.layout.view_model_fragment_full_size_gallery;
}
#Bind(R.id.full_size_image_view) ImageView mImageView;
public static ViewModelFullSizeGalleryFragment newInstance(String url, int position, int
target) {
ViewModelFullSizeGalleryFragment fragment = new ViewModelFullSizeGalleryFragment();
fragment.setUrl(url);
fragment.setIndexAndTarget(position, target);
return fragment;
}
private String mUrl;
private int mPosition;
private int mTarget;
public void setUrl(String url) {
mUrl = url;
}
public void setIndexAndTarget(int position, int target) {
mPosition = position;
mTarget = target;
}
#Override protected void initViewModel(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mImageView.setTransitionName(mUrl);
}
Glide.with(getContext())
.load(mUrl)
.transform(new HotelDetailsImageTransformation(getContext(),
LMTApplication.mWidth))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.skipMemoryCache(true)
.error(R.drawable.ic_nopic)
.into(new SimpleTarget<GlideDrawable>() {
#Override public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
if (mImageView != null && resource != null) {
mImageView.setImageDrawable(resource);
mImageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override public boolean onPreDraw() {
mImageView.getViewTreeObserver().removeOnPreDrawListener(this);
if (mTarget == mPosition)
((ISharedElementCallback) getContext()).onViewReadyForTransition();
return true;
}
});
}
}
});
}
#Override public void onBackPressed() {
//!! Not used.
}
public interface ISharedElementCallback {
void onViewReadyForTransition();
}
public ImageView getImageView() {
return mImageView;
}
Havent figured out what actually does the flickering... Any help will be grand. Thanks.
Also. If its any help theres a map fragment behind the Activity A recycler view if its any help.
Does setting android:windowSharedElementsUseOverlay (inside your Activity's XML theme file) to false solve your problem?
Related
I am using Epoxy Controller for Recycler View. I am having trouble changing the view after data changed by the user action.
Basically I have a switch button in a view which is used inside a recycler view and I am trying to update the view on switch button state change. I am calling requestModelBuild() in setProductList() function of the epoxy controller but change is not reflected in the view.
public class SellerInventoryListEpoxyController extends EpoxyController {
private List<Product> productList = Collections.emptyList();
private Context context;
private SellerInventoryListEpoxyController.Callbacks callbacks;
public void setProductList(List<Product> productList, Context context, SellerInventoryListEpoxyController.Callbacks callbacks) {
this.productList = productList;
this.context = context;
this.callbacks = callbacks;
requestModelBuild();
}
#Override
protected void buildModels() {
for (int i = 0; i < productList.size(); i++) {
new InventoryProductDetailModel_()
.id(productList.get(i).getId())
.product(productList.get(i))
.position(i)
.listSize(productList.size())
.callbacks(callbacks)
.context(context)
.addTo(this);
}
}
public interface Callbacks {
void onViewComboClick(Product productComboList);
void onProductListingStatusChanged(Boolean newStatus, int productSellerId);
void onRecyclerViewReachEnd();
}
}
public class InventoryProductDetailModel extends EpoxyModelWithHolder<InventoryProductDetailModel.ViewHolder> implements CompoundButton.OnCheckedChangeListener {
#EpoxyAttribute
Product product;
#EpoxyAttribute
int position;
#EpoxyAttribute
int listSize;
#EpoxyAttribute
Context context;
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
SellerInventoryListEpoxyController.Callbacks callbacks;
#Override
protected ViewHolder createNewHolder() {
return new ViewHolder();
}
#Override
protected int getDefaultLayout() {
return R.layout.inventroy_item_layout;
}
private DrawableCrossFadeFactory factory =
new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build();
#Override
public void bind(#NonNull InventoryProductDetailModel.ViewHolder holder) {
super.bind(holder);
holder.quantity.setText(String.format("Available :%d", product.getTotalStock()));
holder.brand.setText(product.getProduct().getBrandName());
holder.title.setText(product.getProduct().getTitle());
holder.category.setText(product.getProduct().getCategoryName());
holder.sku.setText(String.format("Sku: %s", product.getSku()));
holder.inventoryItemConstrainLayout.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent intent = new Intent(context, ProductDetailActivity.class);
intent.putExtra("product_id", product.getId());
context.startActivity(intent);
}
});
if (product.getProductCombos() != null && product.getProductCombos().size() > 0) {
holder.variationCount.setVisibility(View.GONE);
holder.comboBtn.setVisibility(View.VISIBLE);
holder.comboBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
callbacks.onViewComboClick(product);
}
});
}
if (product.getSellerActive()) {
holder.productStatusSwitch.setText("Active");
holder.productStatusSwitch.setOnCheckedChangeListener(null);
holder.productStatusSwitch.setChecked(true);
holder.productStatusSwitch.setOnCheckedChangeListener(this);
holder.productStatusSwitch.setTextColor(context.getResources().getColor(R.color.colorAccent));
} else {
holder.productStatusSwitch.setText("Inactive");
holder.productStatusSwitch.setOnCheckedChangeListener(null);
holder.productStatusSwitch.setChecked(false);
holder.productStatusSwitch.setOnCheckedChangeListener(this);
holder.productStatusSwitch.setTextColor(Color.parseColor("#ff0000"));
}
holder.variationCount.setText(format("Variation(%d)", product.getVariantCount()));
holder.variationCount.setVisibility(View.VISIBLE);
holder.comboBtn.setVisibility(View.GONE);
loadImage(holder.productImage, Utils.getRequiredUrlForThisImage(holder.productImage, product.getProduct().getImage()));
if (position == listSize - 2) {
callbacks.onRecyclerViewReachEnd();
}
}
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
callbacks.onProductListingStatusChanged(isChecked, product.getId());
}
private void loadImage(ImageView imageView, String url) {
Glide.with(imageView.getContext()).asBitmap()
.load(Utils.getRequiredUrlForThisImage(imageView, url))
.apply(new RequestOptions().diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.fitCenter())
.transition(withCrossFade(factory))
.placeholder(R.mipmap.product)
.into(imageView);
}
#Override
public void unbind(#NonNull InventoryProductDetailModel.ViewHolder holder) {
super.unbind(holder);
}
public static class ViewHolder extends EpoxyHolder {
TextView quantity, brand, title, category, variationCount, comboBtn;
ImageView productImage, btn_product_detail;
ProgressBar progressBar;
ConstraintLayout inventoryItemConstrainLayout;
private TextView sku;
private Switch productStatusSwitch;
#Override
protected void bindView(#NonNull View itemView) {
productStatusSwitch = itemView.findViewById(R.id.productStatusSwitch);
quantity = itemView.findViewById(R.id.product_qty);
brand = itemView.findViewById(R.id.product_brand);
title = itemView.findViewById(R.id.product_title);
sku = itemView.findViewById(R.id.sku);
category = itemView.findViewById(R.id.product_category);
variationCount = itemView.findViewById(R.id.variantCount);
productImage = itemView.findViewById(R.id.product_image);
btn_product_detail = itemView.findViewById(R.id.btn_product_detail);
inventoryItemConstrainLayout = itemView.findViewById(R.id.inventory_item_constrain_layout);
comboBtn = itemView.findViewById(R.id.combo_btn);
progressBar = itemView.findViewById(R.id.progressbar);
progressBar.setVisibility(View.GONE);
}
}
#Override
public int hashCode() {
super.hashCode();
return product.hashCode();
}
#Override
public boolean equals(Object o) {
return super.equals(o);
}
}
private void addProductListingChangeObserver(final Boolean newStatus, final int productSellerId) {
ProductUpdate productUpdate = new ProductUpdate();
productUpdate.setSellerActive(newStatus);
mInventoryViewModel.updateProductSeller(productSellerId, productUpdate).observe(this, new Observer<Resource<ProductSeller>>() {
#Override
public void onChanged(Resource<ProductSeller> productSellerResource) {
if (productSellerResource.status == Status.ERROR) {
progressBar.setVisibility(View.GONE);
} else if (productSellerResource.status == Status.SUCCESS) {
progressBar.setVisibility(View.GONE);
if (productSellerResource.data != null && productSellerResource.data.isSellerActive() == newStatus) {
for (int i = 0; i < productList.size(); i++) {
if (productList.get(i).getId() == productSellerId) {
productList.get(i).setSellerActive(newStatus);
break;
}
}
sellerInventoryListEpoxyController.setProductList(productList, getContext(), InventoryFragment.this);
}
} else {
progressBar.setVisibility(View.VISIBLE);
}
}
});
}
In addProductListingChangeObserver() function one object of productList is modified and new productList is passed to the EpoxyController and requestModelbuild is called but the view is not modifying as expected.
I have a problem, when i swipe to refresh the data, the first swipe is ok but after that every swipe reload and add the same data over and over again, by the end i have a list with same items over and over... I'm using a loader.
I tried to clear before but i don't understand what's wrong with my code, if someone could explain it to me. Thank You.
Here my code :
public abstract class NewsFragment extends Fragment implements LoaderManager.LoaderCallbacks<ArrayList<Articles>> {
protected ItemAdapter mArticleAdapter;
protected RecyclerView mRecyclerView;
protected NewsFragment.OnNewSelectedInterface mListener;
protected RecyclerView.LayoutManager mManager;
protected SwipeRefreshLayout mSwipeRefreshLayout;
protected LoaderManager mLoaderManager;
private boolean mStateSaved;
private static final int NEWS_LOAD_ID = 1;
public static final String KEY_LIST = "key_list";
public interface OnNewSelectedInterface {
void onListNewSelected(int index, ArrayList<Articles> articles);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.list_present_news, container, false);
mListener = (NewsFragment.OnNewSelectedInterface) getActivity();
mSwipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipeContainer);
mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerview);
mManager = new LinearLayoutManager(getActivity());
mArticleAdapter = new ItemAdapter(getActivity(), new ArrayList<Articles>(), mListener);
mLoaderManager = getLoaderManager();
mStateSaved = mArticleAdapter.isStateSaved();
mRecyclerView.setAdapter(mArticleAdapter);
mRecyclerView.setLayoutManager(mManager);
getData();
refreshData();
if(!isNetworkAvailable())alertUserAboutError();
setDivider();
return view;
}
private void setDivider() {
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mRecyclerView
.getContext(), DividerItemDecoration.VERTICAL);
mRecyclerView.addItemDecoration(dividerItemDecoration);
}
private void getData() {
getLoaderManager().initLoader(NEWS_LOAD_ID, null, this).forceLoad();
}
private void alertUserAboutError() {
AlertDialogFragment alertDialogFragment = new AlertDialogFragment();
alertDialogFragment.show(getActivity().getFragmentManager(), "error_dialog");
}
protected abstract String[] getUrl();
private boolean isNetworkAvailable() {
ConnectivityManager manager = (ConnectivityManager)
getActivity().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
boolean isAvailable = false;
if (networkInfo != null && networkInfo.isConnected()) {
isAvailable = true;
}
return isAvailable;
}
private void refreshData() {
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
mArticleAdapter.clear();
mSwipeRefreshLayout.setRefreshing(false);
}
});
mSwipeRefreshLayout.setColorSchemeResources(
android.R.color.holo_orange_light,
android.R.color.holo_red_light);
}
#Override
public Loader<ArrayList<Articles>> onCreateLoader(int id, Bundle args) {
return new NewsLoader(getActivity(), getUrl());
}
#Override
public void onLoadFinished(Loader<ArrayList<Articles>> loader, ArrayList<Articles> data) {
if (data != null && !data.isEmpty()) {
mArticleAdapter.addAll(data);
}
}
#Override
public void onLoaderReset(Loader<ArrayList<Articles>> loader) {
mArticleAdapter.clear();
}
}
My loader class :
public class NewsLoader extends AsyncTaskLoader<ArrayList<Articles>>{
private ArrayList<Articles> mArticlesArrayList;
private String[] mUrl;
public NewsLoader(Context context, String[] url) {
super(context);
mUrl = url;
}
#Override
public ArrayList<Articles> loadInBackground() {
OkHttpClient mClient = new OkHttpClient();
for (String aMUrl : mUrl) {
Request mRequest = new Request.Builder().url(aMUrl).build();
try {
Response response = mClient.newCall(mRequest).execute();
try {
if (response.isSuccessful()) {
String json = response.body().string();
getMultipleUrls(json);
}
} catch (IOException | JSONException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return mArticlesArrayList;
}
private void getMultipleUrls(String jsonData) throws JSONException {
if (mArticlesArrayList == null) {
mArticlesArrayList = getArticleForecast(jsonData);
} else {
mArticlesArrayList.addAll(getArticleForecast(jsonData));
}
}
private ArrayList<Articles> getArticleForecast(String jsonData) throws JSONException {
JSONObject forecast = new JSONObject(jsonData);
JSONArray articles = forecast.getJSONArray("articles");
ArrayList<Articles> listArticles = new ArrayList<>(articles.length());
for (int i = 0; i < articles.length(); i++) {
JSONObject jsonArticle = articles.getJSONObject(i);
Articles article = new Articles();
String urlImage = jsonArticle.getString("urlToImage");
article.setTitle(jsonArticle.getString("title"));
article.setDescription(jsonArticle.getString("description"));
article.setImageView(urlImage);
article.setArticleUrl(jsonArticle.getString("url"));
listArticles.add(i, article);
}
return listArticles;
}
}
My Adapter class :
public class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ArticleViewHolder> {
private static final String TAGO = ItemAdapter.class.getSimpleName();
private final NewsFragment.OnNewSelectedInterface mListener;
private ArrayList<Articles> mArticlesList;
private Context mContext;
private int lastPosition = -1;
private boolean mStateSaved = false;
public boolean isStateSaved() {
return mStateSaved;
}
public void setStateSaved(boolean stateSaved) {
mStateSaved = stateSaved;
}
public ItemAdapter(Context context, ArrayList<Articles> articles, NewsFragment.OnNewSelectedInterface listener){
mContext = context;
mArticlesList = articles;
mListener = listener;
}
#Override
public ArticleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_card_view, parent, false);
ArticleViewHolder articleViewHolder = new ArticleViewHolder(view);
articleViewHolder.setIsRecyclable(false);
return articleViewHolder;
}
#Override
public void onBindViewHolder(ArticleViewHolder holder, int position) {
holder.bindArticle(mArticlesList.get(holder.getAdapterPosition()));
setAnimation(holder.itemView, holder.getAdapterPosition());
}
private void setAnimation(View viewToAnimate, int position) {
if (position > lastPosition) {
Animation animation = AnimationUtils.loadAnimation(viewToAnimate.getContext(), android.R.anim.slide_in_left);
viewToAnimate.startAnimation(animation);
lastPosition = position;
}
}
#Override
public int getItemCount() {
return mArticlesList.size();
}
public void clear() {
mArticlesList.clear();
notifyDataSetChanged();
}
public void addAll(ArrayList<Articles> articles) {
mArticlesList.addAll(articles);
notifyDataSetChanged();
}
protected class ArticleViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
private ImageView mImageView;
private TextView mTitleTextView, mDescriptionTextView;
private FloatingActionButton mSaveButton;
private ArticleViewHolder(View itemView) {
super(itemView);
mImageView = (ImageView) itemView.findViewById(R.id.photoImageView);
mTitleTextView = (TextView) itemView.findViewById(R.id.titleWithoutImage);
mDescriptionTextView = (TextView) itemView.findViewById(R.id.descriptionTextView);
mSaveButton = (FloatingActionButton) itemView.findViewById(R.id.floatingActionButton);
itemView.setOnClickListener(this);
}
private void bindArticle(final Articles article) {
Glide.with(mContext).load(article.getImageView()).into(mImageView);
mTitleTextView.setText(article.getTitle());
mDescriptionTextView.setText(article.getDescription());
if(mDescriptionTextView.getText().equals("")){
mDescriptionTextView.setVisibility(View.GONE);
}
mSaveButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
insertArticle(article);
article.setStateSaved(true);
}
});
Log.v(TAGO, "Item id : " + getItemId()
+ "Item count : " + getItemCount()
+ "Item position : " + getAdapterPosition()
+ String.valueOf(article.isStateSaved()));
}
private void insertArticle(Articles articles) {
String title = articles.getTitle();
String description = articles.getDescription();
String url = articles.getArticleUrl();
ContentValues contentValues = new ContentValues();
contentValues.put(ArticleContract.ArticleEntry.COLUMN_TITLE_ARTICLE, title);
contentValues.put(ArticleContract.ArticleEntry.COLUMN_DESCRIPTION_ARTICLE, description);
contentValues.put(ArticleContract.ArticleEntry.COLUMN_URL_ARTICLE, url);
Uri uri = mContext.getContentResolver().insert(ArticleContract.ArticleEntry.CONTENT_URI, contentValues);
if(uri == null) {
Log.v(TAGO, "Error");
} else Toast.makeText(mContext, "Article Saved", Toast.LENGTH_SHORT).show();
}
#Override
public void onClick(View view) {
mListener.onListNewSelected(getLayoutPosition(), mArticlesList);
}
}
}
You are using ViewHolder#setIsRecyclable incorrectly; this method is meant to be used to prevent a ViewHolder from being recycled only while changes are being made to it. According to the documentation:
Calls to setIsRecyclable() should always be paired (one call to
setIsRecyclabe(false) should always be matched with a later call to
setIsRecyclable(true)).
This means none of your ViewHolder objects will be recycled, effectively making the use of a RecyclerView worthless, and preventing it from reusing the views when you attempt to bind new objects to your RecyclerView.
So, in short, remove that line of code.
I noticed a few other small issues with your adapter code as well, which can cause a multitude headaches in the future; so I took the liberty of highlighting some of the changes I would make.
Just for my own sanity, I will refer to your Articles class as Article.
It is usually not a good idea to pass around your Context all over the place. The View passed to your ViewHolder already has a reference to a Context, so you can use that instead.
As for the insertArticle() code, the Activity should be handling this anyway. So you can pass the Article back to the Activity by passing a listener to your Adapter (and subsequently, each ViewHolder) instead of the Context.
You should also consider using the DiffUtil class instead of just calling notifyDataSetChanged(); it is much more efficient. Just make sure your Article class is implementing equals() and hashCode() or it will not work.
I didn't include the animation code (that can easily be added back in) or the saved state code (mostly because I don't know what you were trying to do).
public class ArticleAdapter extends RecyclerView.Adapter<Article> {
private List<Article> mData;
private ArticleViewHolder.OnSelectedListener mOnSelectedListener;
private ArticleViewHolder.OnSaveListener mOnSaveListener;
public ArticleAdapter(ArticleViewHolder.OnSelectedListener onSelectedListener, ArticleViewHolder.OnSaveListener onSaveListener) {
mOnSelectedListener = onSelectedListener;
mOnSaveListener = onSaveListener;
mData = new ArrayList<>();
}
public void replaceData(final List<Article> data) {
final List<Article> oldData = new ArrayList<>(mData);
mData.clear();
if (data != null) {
mData.addAll(data);
}
DiffUtil.calculateDiff(new DiffUtil.Callback() {
#Override
public int getOldListSize() {
return oldData.size();
}
#Override
public int getNewListSize() {
return mData.size();
}
#Override
public int areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldData.get(oldItemPosition).equals(mData.get(newItemPosition));
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldData.get(oldItemPosition).equals(mData.get(newItemPosition));
}
}).dispatchUpdatesTo(this);
}
#Override
public ArticleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_card_view, parent, false);
return new SelectLocationViewHolder(view, mOnSelectedListener, mOnSaveListener);
}
#Override
public void onBindViewHolder(ArticleViewHolder holder, int position) {
holder.bind(mData.get(position));
}
#Override
public int getItemCount() {
return mData.size();
}
}
public class ArticleViewHolder extends RecyclerView.ViewHolder {
public interface OnSelectedListener {
void onSelected(Article article);
}
public interface OnSaveListener {
void onSave(Article article);
}
private View mView;
private Article mArticle;
private OnSelectedListener mOnSelectedListener;
private OnSaveListener mOnSaveListener;
private ImageView mImageView;
private TextView mTitleTextView, mDescriptionTextView;
private FloatingActionButton mSaveButton;
public ArticleViewHolder(View itemView, final OnSelectedListener onSelectedListener, final OnSaveListener onSaveListener) {
super(itemView);
mImageView = (ImageView) itemView.findViewById(R.id.photoImageView);
mTitleTextView = (TextView) itemView.findViewById(R.id.titleWithoutImage);
mDescriptionTextView = (TextView) itemView.findViewById(R.id.descriptionTextView);
mSaveButton = (FloatingActionButton) itemView.findViewById(R.id.floatingActionButton);
mView = itemView;
mView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
onSelectedListener.onSelected(mArticle);
}
});
mSaveButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
onSaveListener.onSave(mArticle);
}
});
}
public void bind(Article article) {
mArticle = article;
mTitleTextView.setText(article.getTitle());
mDescriptionTextView.setText(article.getDescription());
if(TextUtils.isEmpty(article.getDescription())) {
mDescriptionTextView.setVisibility(View.GONE);
}
Glide.with(mView.getContext()).load(article.getImage()).into(mImageView);
}
}
Edit
The actual issue is that your loader uses the same ArrayList every time, and keeps adding the new results to it.
public class NewsLoader extends AsyncTaskLoader<List<Article>> {
private final String[] mUrls;
private final OkHttpClient mClient;
public NewsLoader(Context context, OkHttpClient client, String... urls) {
super(context);
mClient = client;
mUrls = urls;
}
#Override
public List<Article> loadInBackground() {
List<Article> articles = new ArrayList<>();
for (String url : mUrls) {
Request request = new Request.Builder().url(url).build();
try {
Response response = mClient.newCall(request).execute();
if (response.isSuccessful()) {
parseData(response.body().string(), articles);
}
} catch (IOException | JSONException e) {
e.printStackTrace();
}
}
return articles;
}
private void parseData(List<Article> articles, String data) throws JSONException {
JSONObject forecast = new JSONObject(data);
JSONArray a = forecast.getJSONArray("articles");
for (int i = 0; i < a.length(); i++) {
JSONObject o = a.getJSONObject(i);
Article article = new Article(
o.getString("title"),
o.getString("description"),
o.getString("url"),
o.getString("urlToImage"));
articles.add(article);
}
}
}
Also, you may have noticed, I made a small change to your Article constructor. You should consider making the Article class immutable, as this will prevent you from making mistakes when dealing with multithreading. It should look something like this:
public class Article {
private final String mTitle;
private final String mDescription;
private final String mUrl;
private final String mImageUrl;
public Article(String title, String description, String url, String imageUrl) {
mTitle = title;
mDescription = description;
mUrl = url;
mImageUrl = imageUrl;
}
public String title() {
return mTitle;
}
public String description() {
return mDescription;
}
public String url() {
return mUrl;
}
public String imageUrl() {
return mImageUrl;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Article other = (Article) o;
return mTitle != null && mTitle.equals(other.mTitle) &&
mDescription != null && mDescription.equals(other.mDescription) &&
mUrl != null && mUrl.equals(other.mUrl) &&
mImageUrl != null && mImageUrl.equals(other.mImageUrl);
}
#Override
public int hashCode() {
int result = mTitle != null ? mTitle.hashCode() : 0;
result = 31 * result + (mDescription != null ? mDescription.hashCode() : 0);
result = 31 * result + (mUrl != null ? mUrl.hashCode() : 0);
result = 31 * result + (mImageUrl != null ? mImageUrl.hashCode() : 0);
return result;
}
}
#Override
public void onBindViewHolder(ArticleViewHolder holder, int position) {
holder.bindArticle(mArticlesList.get(position));
setAnimation(holder.itemView, position);
}
public void addAll(ArrayList<Articles> articles) {
mArticlesList.clear();
mArticlesList.addAll(articles);
notifyDataSetChanged();
}
If this doesn't wrok then I think your api is giving you redundant data.
Why you are using articleViewHolder.setIsRecyclable(false);
One another place which might cause the problem is
private void getMultipleUrls(String jsonData) throws JSONException {
if (mArticlesArrayList == null) {
mArticlesArrayList = getArticleForecast(jsonData);
} else {
mArticlesArrayList.addAll(getArticleForecast(jsonData));
}
}
You are calling it from a loop add adding data to your arraylist. There somehow multiple data can be inserted in your ArrayList
I have a Fragment with RecyclerView, which holds items with ImageView (basically like gallery app). Images are displayed using async task to do the work in separate thread. Images are displayed from base64 encoded string. Images are also cached with lrucache.
Problem
Everything works fine until i rotate the device 3rd or 4th time. The device crashes with out of memory error in onSaveInstanceState method.
Question
Any ideas how to prevent the OutOfMemory error? Thanks in advance
Code
Activity
public class TabsActivity extends BaseActivity implements ViewPager.OnPageChangeListener,
ActivityActions, TabLayout.OnTabSelectedListener {
private static final String TAG = TabsActivity.class.getSimpleName();
private static final String STATE_TAB_LAYOUT = "STATE_TAB_LAYOUT";
private static final String STATE_TOOLBAR = "STATE_TOOLBAR";
public static final String ACTION_TASKS = "ACTION_TASKS";
public static final String ACTION_MESSAGES = "ACTION_MESSAGES";
public static final int REQUEST_TASK_UPDATE = 1;
public static final int REQUEST_MESSAGE_UPDATE = 2;
private Toolbar toolbar;
private TabLayout tabLayout;
private ViewPager viewPager;
private FloatingActionButton createButton;
private PrefsManager prefsManager;
private ViewPagerAdapter adapter;
private LocalBroadcastManager broadcastManager;
private NotificationReceiver notificationReceiver;
private FragmentManager.OnBackStackChangedListener backStackChangedListener;
#Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tabs);
prefsManager = PrefsManager.getInstance(this);
notificationReceiver = new NotificationReceiver();
backStackChangedListener = new FragmentManager.OnBackStackChangedListener() {
#Override
public void onBackStackChanged() {
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
onSettingsFragmentStateChanged(false);
}
}
};
tabLayout = (TabLayout) findViewById(R.id.tab_layout);
toolbar = (Toolbar) findViewById(R.id.toolbar);
viewPager = (ViewPager) findViewById(R.id.view_pager);
createButton = (FloatingActionButton) findViewById(R.id.floating_action_button);
createButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
KeyboardUtils.hideSoftwareInput(TabsActivity.this);
if (adapter.getCurrentTab(viewPager.getCurrentItem()) == Tab.TASKS) {
createNewTask();
} else {
createNewConversation();
}
}
});
setSupportActionBar(toolbar);
toolbar.setNavigationIcon(R.drawable.ic_action_back);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onBackPressed();
}
});
final String action = getIntent().getAction();
adapter = new ViewPagerAdapter(getSupportFragmentManager());
adapter.setAdapterListener(new ExtendedPagerAdapter.AdapterListener() {
#Override
public void onAdapterInstantiated() {
if (savedInstanceState == null && action != null) {
if (action.equals(ACTION_TASKS)) {
onPageSelected(viewPager.getCurrentItem());
} else {
viewPager.setCurrentItem(1);
}
}
}
});
viewPager.addOnPageChangeListener(this);
viewPager.setAdapter(adapter);
tabLayout.setupWithViewPager(viewPager);
tabLayout.setOnTabSelectedListener(this);
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_TASK_UPDATE) {
if (resultCode == Activity.RESULT_OK) {
((TasksFragment) adapter.getFragment(Tab.TASKS)).onTasksUpdated();
}
} else if (requestCode == REQUEST_MESSAGE_UPDATE) {
TabFragment tabFragment = adapter.getFragment(Tab.MESSAGES);
if (resultCode == Activity.RESULT_OK) {
if (tabFragment != null) {
((MessagesFragment) tabFragment).onNewMessagesReceived();
}
} else {
((MessagesFragment) tabFragment).initData();
}
}
super.onActivityResult(requestCode, resultCode, data);
}
#Override
protected void onResume() {
broadcastManager = LocalBroadcastManager.getInstance(this);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BroadcastConfig.ACTION_NEW_MESSAGE);
intentFilter.addAction(BroadcastConfig.ACTION_USER_STATUS);
broadcastManager.registerReceiver(notificationReceiver, intentFilter);
getSupportFragmentManager().addOnBackStackChangedListener(backStackChangedListener);
super.onResume();
}
#Override
protected void onPause() {
broadcastManager.unregisterReceiver(notificationReceiver);
getSupportFragmentManager().removeOnBackStackChangedListener(backStackChangedListener);
super.onPause();
}
#Override
protected void onRestoreInstanceState(Bundle inState) {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(inState.getString(STATE_TOOLBAR));
}
if (!inState.getBoolean(STATE_TAB_LAYOUT)) {
onSettingsFragmentStateChanged(true);
}
super.onRestoreInstanceState(inState);
}
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean(STATE_TAB_LAYOUT, tabLayout.getVisibility() == View.VISIBLE);
outState.putString(STATE_TOOLBAR, toolbar.getTitle().toString());
super.onSaveInstanceState(outState);
}
#Override
public void onBackPressed() {
MenuItem menuItem = toolbar.getMenu().findItem(R.id.action_search);
if (menuItem != null && !((SearchView) menuItem.getActionView()).isIconified()) {
((SearchView) menuItem.getActionView()).onActionViewCollapsed();
return;
}
if (popSupportBackStack(SettingsFragment.class.getSimpleName())) {
return;
}
setResult(RESULT_OK);
finish();
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_contacts, menu);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
return false;
}
#Override
public boolean onQueryTextChange(String newText) {
adapter.getFragment(viewPager.getCurrentItem()).onSearchPhraseChanged(newText);
return true;
}
});
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
onSettingsClick();
break;
case R.id.action_refresh:
onRefreshClick();
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
#Override
public void onPageSelected(int position) {
if (adapter.isInstantiated()) {
onToolbarTitleChanged(adapter.getPageTitle(position).toString());
onToolbarSubtitleChanged(UserStatus.NONE);
}
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageScrollStateChanged(int state) {
}
#Override
public void onTabSelected(TabLayout.Tab tab) {
if (adapter.getCurrentTab(tab.getPosition()).getFragmentTitle()
== Tab.ATTACHMENT_HISTORY.getFragmentTitle()) {
createButton.hide();
}
viewPager.setCurrentItem(tab.getPosition());
}
#Override
public void onTabUnselected(final TabLayout.Tab tab) {
createButton.hide(new FloatingActionButton.OnVisibilityChangedListener() {
#Override
public void onHidden(FloatingActionButton fab) {
fab.show();
}
});
}
#Override
public void onTabReselected(TabLayout.Tab tab) {
}
#Override
public void requestDisplayDetails(Intent intent, int requestCode) {
startActivityForResult(intent, requestCode);
}
#Override
public void refreshTasks() {
adapter.getFragment(Tab.TASKS).onRefresh();
}
#Override
public void refreshMessages() {
adapter.getFragment(Tab.MESSAGES).onRefresh();
}
#Override
public boolean isNetworkAvailable() {
return checkNetworkAvailability();
}
private void createNewTask() {
startActivityForResult(new Intent(this, CreateTaskActivity.class), REQUEST_TASK_UPDATE);
}
private void createNewConversation() {
MessagesFragment fragment = (MessagesFragment) adapter.getFragment(Tab.MESSAGES);
startActivityForResult(new Intent(TabsActivity.this, MessageDetailsActivity.class)
.setAction(MessageDetailsActivity.ACTION_CREATE_MESSAGE)
.putExtra(MessageDetailsActivity.EXTRA_EXIST_CONV,
fragment.getCreatedConversations()), REQUEST_MESSAGE_UPDATE);
}
private void setTabLayoutVisible(boolean visible) {
int visibility = visible ? View.VISIBLE : View.GONE;
tabLayout.setVisibility(visibility);
}
private void onRefreshClick() {
if (checkNetworkAvailability()) {
adapter.getFragment(viewPager.getCurrentItem()).onRefresh();
}
}
private void onSettingsClick() {
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, new SettingsFragment(), SettingsFragment.class.getSimpleName())
.addToBackStack(SettingsFragment.class.getSimpleName())
.commit();
onSettingsFragmentStateChanged(true);
}
private void onSettingsFragmentStateChanged(boolean visibleState) {
if (visibleState) {
isFragmentDialog = true;
onToolbarTitleChanged(getString(R.string.title_settings));
setTabLayoutVisible(false);
createButton.hide();
} else {
onPageSelected(viewPager.getCurrentItem());
setTabLayoutVisible(true);
if (adapter.getCurrentTab(viewPager.getCurrentItem()) != Tab.ATTACHMENT_HISTORY) {
createButton.show();
}
}
}
public void onToolbarTitleChanged(String title) {
toolbar.setTitle(title);
}
public void onToolbarSubtitleChanged(UserStatus userStatus) {
if (userStatus != null) {
toolbar.setSubtitle(userStatus.getText());
toolbar.setSubtitleTextColor(userStatus.getColor());
}
}
public void onLogoutConfirmed() {
HttpRequestManager.logout(prefsManager.getPhpSessId(), App.getPhoneId(this), prefsManager.getUserId(),
new HttpCallback<LogoutResponse>() {
#Override
public void onResponse(LogoutResponse logoutResponse) {
prefsManager.reset();
DBManager.delete(TabsActivity.this, DeleteTask.DeleteType.ALL, new Callback<Boolean>() {
#Override
public void onResponseReceived(Boolean params) {
startActivity(new Intent(TabsActivity.this, LoginActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK));
finish();
}
});
}
});
}
private class NotificationReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(BroadcastConfig.ACTION_NEW_MESSAGE) || action.equals(BroadcastConfig.ACTION_USER_STATUS)) {
TabFragment childFragment = adapter.getFragment(Tab.MESSAGES);
if (childFragment != null) {
((MessagesFragment) childFragment).onNewMessagesReceived();
}
}
}
}
private class ViewPagerAdapter extends ExtendedPagerAdapter {
private final List<Tab> tabs = Tab.getAllTabs();
private final List<TabFragment> fragments = new ArrayList<>();
public ViewPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
fragments.add((TabFragment) super.instantiateItem(container, position));
return fragments.get(fragments.size() - 1);
}
#Override
public Fragment getItem(int position) {
Log.e(TAG, "CreatingFragment: " + tabs.get(position).getFragmentClass().getCanonicalName());
return Fragment.instantiate(TabsActivity.this, tabs.get(position).getFragmentClass().getCanonicalName());
}
#Override
public CharSequence getPageTitle(int position) {
return getString(tabs.get(position).getFragmentTitle());
}
#Override
public int getCount() {
return tabs.size();
}
public Tab getCurrentTab(int position) {
return tabs.get(position);
}
public TabFragment getFragment(int position) {
if (getCount() > position) {
return fragments.get(position);
}
return null;
}
public TabFragment getFragment(Tab tab) {
for (TabFragment tabFragment : fragments) {
if (tab.getFragmentClass() == tabFragment.getClass()) {
return tabFragment;
}
}
return null;
}
}
}
Fragment
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
swipeRefresh = (SwipeRefresh) inflater.inflate(R.layout.fragment_recycler_view, container, false);
galleryView = (RecyclerView) swipeRefresh.findViewById(R.id.recycler_view);
return swipeRefresh;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
swipeRefresh.setOnRefreshListener(this);
galleryView.setHasFixedSize(true);
galleryView.setLayoutManager(new GridLayoutManager(getContext(), getSpanCount()));
galleryView.setAdapter(adapter = new Adapter(attachments));
if (savedInstanceState == null) {
onRefresh();
} else {
onRestoreInstanceState(savedInstanceState);
}
}
#Override
public void onSaveInstanceState(Bundle outState) {
try {
outState.putString(STATE_ATTACHMENTS, Json.fromObject(attachments)); //Crashes here after 3rd or 4th rotate
} catch (JsonProcessingException e) {
Log.e(TAG, "Error while saving attachments", e);
}
super.onSaveInstanceState(outState);
}
#Override
public void onSearchPhraseChanged(String phrase) {
}
#Override
public void onRefresh() {
swipeRefresh.setRefreshing(true);
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
initSampleData();
}
}, 3000);
}
private void onRestoreInstanceState(Bundle savedInstanceState) {
try {
attachments = Json.toCollection(savedInstanceState.getString(STATE_ATTACHMENTS),
ArrayList.class, Attachment.class);
adapter.notifyDataSetChanged();
} catch (IOException e) {
Log.e(TAG, "Error while restoring attachments", e);
}
}
private void initSampleData() {
swipeRefresh.setRefreshing(false);
String image = "";
File path = new File(Environment.getExternalStorageDirectory(), "image.txt");
byte[] bytes = new byte[(int) path.length()];
try {
FileInputStream fileInputStream = new FileInputStream(path);
fileInputStream.read(bytes);
image = new String(bytes);
} catch (FileNotFoundException e) {
Log.e(TAG, "Error while finding file to read from", e);
} catch (IOException e) {
Log.e(TAG, "Error while writing from file to string", e);
}
Attachment attachment = new Attachment(image);
attachments.clear();
for (int i = 0; i < 100; i++) {
attachment.setFileName(String.valueOf(i));
attachments.add(attachment);
}
adapter.notifyDataSetChanged();
}
private int getSpanCount() {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
float width = displayMetrics.widthPixels / displayMetrics.density;
float height = displayMetrics.heightPixels / displayMetrics.density;
int size;
if (Math.min(width, height) >= 600) {
size = Math.round(width / THUMBNAIL_SIZE_TABLET);
} else {
size = Math.round(width / THUMBNAIL_SIZE_PHONE);
}
return size < 7 ? size : 6;
}
private class ViewHolder extends BaseHolder<Attachment> implements View.OnClickListener {
private ImageView imageView;
public ViewHolder(ViewGroup viewGroup, int layoutRes) {
super(viewGroup, layoutRes);
imageView = (ImageView) itemView;
}
#Override
public void bind(Attachment attachment) {
itemView.setOnClickListener(this);
if (attachment.getImage() != null && !attachment.getImage().isEmpty()) {
DisplayThumbnailRequest.loadBitmap(attachment, imageView);
}
}
#Override
public void onClick(View v) {
// TODO: 2016-01-15 Open image in fullscreen
}
}
private class Adapter extends RecyclerView.Adapter<ViewHolder> {
private List<Attachment> attachments;
public Adapter(List<Attachment> attachments) {
this.attachments = attachments;
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(parent, R.layout.adapter_item_attachment_history);
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(attachments.get(position));
}
#Override
public int getItemCount() {
return attachments.size();
}
}
AsyncTask
public class DisplayThumbnailRequest extends AsyncHttpTask<Attachment, Void, Bitmap> {
private static final String TAG = DisplayThumbnailRequest.class.getSimpleName();
private WeakReference<ImageView> imageViewRef;
private Attachment attachment;
public DisplayThumbnailRequest(ImageView imageView) {
this.imageViewRef = new WeakReference<>(imageView);
}
#Override
protected void onPreExecute() {
if (imageViewRef != null) {
ImageView imageView = imageViewRef.get();
if (imageView != null) {
if (imageView.getVisibility() != View.VISIBLE) {
imageView.setVisibility(View.VISIBLE);
}
imageView.setImageResource(R.mipmap.ic_launcher);
}
}
}
#Override
protected Bitmap doInBackground(Attachment... params) {
attachment = params[0];
Bitmap bitmap = BitmapCache.getBitmap(attachment.getFileName());
if (bitmap == null) {
bitmap = BitmapUtils.fromBase64(attachment.getImage());
BitmapCache.addBitmap(attachment.getFileName(), bitmap);
}
return bitmap;
}
#Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewRef != null && bitmap != null) {
ImageView imageView = imageViewRef.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
#Override
protected void onResponseReceived() {
}
public Attachment getAttachment() {
return attachment;
}
public static void loadBitmap(Attachment attachment, ImageView imageView) {
if (cancelDownloadRequest(attachment, imageView)) {
DisplayThumbnailRequest request = new DisplayThumbnailRequest(imageView);
AsyncBitmapDrawable drawable = new AsyncBitmapDrawable(App.getRes(), null, request);
imageView.setImageDrawable(drawable);
request.execute(attachment);
}
}
private static boolean cancelDownloadRequest(Attachment attachment, ImageView imageView) {
DisplayThumbnailRequest request = getDownloadTask(imageView);
if (request != null) {
String filePath = request.getAttachment().getFileName();
if (filePath == null || filePath.isEmpty() || !filePath.equals(attachment.getFileName())) {
request.cancel(true);
} else {
return false;
}
}
return true;
}
private static DisplayThumbnailRequest getDownloadTask(ImageView imageView) {
if (imageView != null) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncBitmapDrawable) {
return ((AsyncBitmapDrawable) drawable).getDisplayThumbnailRequest();
}
}
return null;
}
}
BitmapCache
public class BitmapCache {
private static final String TAG = BitmapCache.class.getSimpleName();
private static final int MAX_MEMORY = (int)((Runtime.getRuntime().maxMemory() / 1024) / 4);
private static LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(MAX_MEMORY) {
#Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount() / 1024;
}
};
public static void addBitmap(String key, Bitmap bitmap) {
if (getBitmap(key) == null) {
lruCache.put(key, bitmap);
}
}
public static Bitmap getBitmap(String key) {
return lruCache.get(key);
}
}
BitmapUtils.fromBase64
public static Bitmap fromBase64(String string) {
if (string != null && !string.isEmpty()) {
byte[] decodedString = Base64.decode(string, Base64.DEFAULT);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length, options);
options.inSampleSize = getInSampleSize(options, Metrics.dp2px(100), Metrics.dp2px(100));
options.inJustDecodeBounds = false;
return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length, options);
}
return null;
}
EDIT
I tried reducing arraylist size which is saved in onSavedInstanceState from 100 to 50 and OOM error is not being displayed (despite how many times you rotate the device). Can it be, that saved instance is too long (if it list has 100 items) and it floods the memory?
The problem is located in your
onRefresh() method.
Your should remove all calbacks from handler if activity is beeing stopped.
Try this:
private Handler handler = new Handler();
#Override
public void onRefresh() {
swipeRefresh.setRefreshing(true);
handler.postDelayed(new Runnable() {
#Override
public void run() {
initSampleData();
}
}, 3000);
}
#Override
protected void onStop() {
super.onStop();
handler.removeCallbacksAndMessages(null);
}
I'm developing android App. My app is showing many of pictures from server by image_url with ImageLoader Library such as Picasso, Glide or AUIL.
There are five fragments in main activity. and each of fragments have many image list view.
My question is that after I click specific image view, new activity is created, and then fragment is created that have many images list view.
I clicked different images, and new activities are created. When this gesture occurred , heap memory is not fetched ...
Among of three libraries, AUIL is the best of them.
I tried recursing method at base adapter and drawable callback to null etc...
But This situation was not solved.
Here is my example Adapter sourse:
public class MagazineRelBrandItemAdapter extends BaseAdapter {
ArrayList<String> receivedList;
ArrayList<View> brandThumList = new ArrayList<>();
PrintLog printLog = new PrintLog(Application.isDEBUG(), "MagazineRelBrandItemAdap ", "created");
Activity mActivity;
public MagazineRelBrandItemAdapter(ArrayList<String> dataList, Activity activity) {
receivedList = dataList;
mActivity = activity;
}
#Override
public int getCount() {
return receivedList.size();
}
#Override
public String getItem(int position) {
return receivedList.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
MagazineRelBrandView itemView;
if (convertView == null) {
itemView = new MagazineRelBrandView(Application.getContext(),mActivity);
} else {
itemView = (MagazineRelBrandView) convertView;
}
try {
itemView.setContent(receivedList.get(position));
brandThumList.add(itemView);
} catch (OutOfMemoryError e) {
if (mRecycleList.size() <= parent.getChildCount()) {
printLog.cutsomPrintLog(Application.isDEBUG(), "img recycle size comment>>", mRecycleList.size() + "");
throw e;
}
recycleHalf();
System.gc();
return getView(position, itemView, parent);
}
mRecycleList.add(new WeakReference<View>(itemView));
return itemView;
}
private List<WeakReference<View>> mRecycleList = new ArrayList<WeakReference<View>>();
public void recycleHalf() {
int halfSize = mRecycleList.size() / 2;
List<WeakReference<View>> recycleHalfList = mRecycleList.subList(0, halfSize);
RecycleUtils.recursiveRecycle(recycleHalfList);
for (int i = 0; i < halfSize; i++)
mRecycleList.remove(0);
}
public void recycle() {
RecycleUtils.recursiveRecycle(mRecycleList);
}
}
And Here is my widget :
public class MagazineRelBrandView extends LinearLayout {
Activity mActivity;
public MagazineRelBrandView(Context context,Activity activity) {
super(context);
mActivity = activity;
init();
}
CircleImageView brandLogo;
TextView brandName;
ImageView brandBg;
View clickV;
private void init() {
inflate(getContext(), R.layout.magazine_rel_brand_view, this);
brandBg = (ImageView) findViewById(R.id.magazine_rel_brand_bg);
brandName = (TextView) findViewById(R.id.magazine_rel_brand_name);
brandLogo = (CircleImageView) findViewById(R.id.magazine_rel_brand_icon);
clickV = findViewById(R.id.content_click_view);
brandName.setTypeface(Typeface.createFromAsset(getContext().getAssets(), CommonKeys.FuturaFont));
clickV.setOnClickListener(goBrandDetailPageListener);
}
public void setContent(String brandName) {
this.brandName.setText(brandName);
try {
String brandTitleUrl;
brandTitleUrl = URLEncoder.encode(brandName, "UTF-8");
totalThumbUrl = CommonKeys.brandThumbUrl + brandTitleUrl;
totalBackUrl = CommonKeys.brandBackgroundUrl + brandTitleUrl;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
showBG(brandName);
showLogo(brandName);
}
String totalThumbUrl;
String totalBackUrl;
public void showLogo(final String brandName){
Glide.with(mActivity).load(totalThumbUrl).override(DpToPxUtil.dp2px(38),DpToPxUtil.dp2px(38)).into(brandLogo);
}public void showBG(final String brandName){
Glide.with(mActivity).load(totalBackUrl).override(DpToPxUtil.dp2px(120),DpToPxUtil.dp2px(144)).into(brandBg);
}
public String getBrandName() {
return brandName.getText().toString();
}
Handler brandHandler = new Handler() {
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
MyToast.show("" + msg.obj);
break;
case 1:
BrandInfoZip brandInfoZip = (BrandInfoZip) ((BrandInfoParentZip) msg.obj).getData();
BrandListZip getBrandZip = new BrandListZip();
getBrandZip.setIs_following(brandInfoZip.is_following());
getBrandZip.setBrand_name(getBrandName());
Intent goBrandPage = new Intent(Application.getContext(), BrandDetailActivity.class);
goBrandPage.putExtra("brandZip", getBrandZip);
goBrandPage.putExtra("title", getBrandName());
goBrandPage.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Application.getContext().startActivity(goBrandPage);
break;
}
}
};
OnClickListener goBrandDetailPageListener = new OnClickListener() {
#Override
public void onClick(View v) {
BrandDescriptionZip brandDescriptionZip = new BrandDescriptionZip();
brandDescriptionZip.setBrand_name(getBrandName());
new GetBrandInfoFromServer(brandHandler).execute(brandDescriptionZip);
}
};
}
And fragment onDestroyView sourse :
#Override
public void onStop() {
super.onStop();
if (upperBgImgV.getDrawable() != null) {
upperBgImgV.getDrawable().setCallback(null);
}
if (writerThumb.getDrawable() != null) {
writerThumb.getDrawable().setCallback(null);
}
if (otherContentV.getDrawable() != null) {
otherContentV.getDrawable().setCallback(null);
}
}
#Override
public void onDestroyView() {
Glide.clear(writerThumb);
magazineRelBrandItemAdapter.recycle();
unbindDrawables(getView());
//unbindDrawables(writerThumb);
//unbindDrawables(otherContentV);
System.gc();
super.onDestroyView();
Log.i(TAG, "onDestroyView-");
}
private void unbindDrawables(View view) {
if (view == null)
return;
if (view instanceof ImageView) {
((ImageView) view).setImageDrawable(null);
}
if (view.getBackground() != null) {
view.getBackground().setCallback(null);
}
if (view instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
unbindDrawables(((ViewGroup) view).getChildAt(i));
}
view.setBackgroundResource(0);
view.setBackgroundDrawable(null);
}
}
and Here is activity onDestroy() course :
#Override
protected void onDestroy() {
RecycleUtils.recursiveRecycle(getWindow().getDecorView());
System.gc();
Glide.get(this).clearMemory();
FlurryAgent.onEndSession(Application.getContext());
printLog.cutsomPrintLog(Application.isDEBUG(),"Magazine Detail Activity onDestroy-","onDestroy-");
super.onDestroy();
finish();
}
These course is used to clear memory heap. But Memory leak is continue....
Please help me.
I have a FragmentActivity with a FragmentMediaOverview containing a list of MediaItemViews (each with a imageview and some text) and a click on one of the items opening a detail-Fragment.
Now when I go back (via back button) and forth (click on listitem) several times from list to detail fragment I eventually run into OOM-Errors. I use SoftReferences for the bitmaps in the listitems as well as in the detail fragment.
According to MAT there is an incresing number of MediaItemViews as well as FragmentMediaOverview instances, but I just cannot figure out why.
I read this Android: AlertDialog causes a memory leak , but couldn't solve it nulling out listeners.
Here is my code:
FragmentMediaOverview.java
(This is not a ListFragment because for a tablet-layout the MediaAdapter needs to connect to a gridview)
public class FragmentMediaOverview extends Fragment {
private static String TAG = FragmentMediaOverview.class.getSimpleName();
private MediaAdapter adapter;
private OnMediaSelectedListener selListener;
private ArrayList<BOObject> mediaItems;
private ViewGroup layoutContainer;
private AdapterView itemContainer; // list or gridview
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Log.d(TAG, "onCreateView");
layoutContainer = (ViewGroup) inflater.inflate(R.layout.fragment_media_overview, null);
return layoutContainer;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
selListener = (OnMediaSelectedListener) activity;
}
#Override
public void onDestroy() {
super.onDestroy();
itemContainer.setOnItemClickListener(null);
selListener = null;
adapter = null;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
initUi(layoutContainer);
displayMedia();
}
private void initUi(ViewGroup layoutContainer) {
itemContainer = (AdapterView) layoutContainer.findViewById(android.R.id.list);
itemContainer.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
BOMedia mediaItem = ((BOMedia) mediaItems.get(position));
//the FragmentActivity is coordinating the FragmentTransactions
selListener.onMediaSelected(mediaItem);
}
});
}
private void displayMedia() {
Log.d(TAG, "Displaying List");
if (mediaItems == null) {
loadMedia();
return;
}
Log.d(TAG, "List: " + mediaItems.size() + ", adapter: " + itemContainer.getAdapter());
if (adapter == null) {
Log.d(TAG, "Create Adapter with " + mediaItems.size());
adapter = new MediaAdapter(getActivity(), mediaItems);
}
if (itemContainer.getAdapter() == null) {
itemContainer.setAdapter(adapter);
} else {
adapter.setItems(mediaItems);
adapter.notifyDataSetChanged();
}
}
private void loadMedia() {
FragmentHelper.showProgressSpinner(layoutContainer, android.R.id.list);
DbHelper.getInstance().getMedia(mediaType, new DbQueryFinishListener() {
#Override
public void onDbCallFinish(ArrayList<BOObject> objects) {
if (!getActivity().isFinishing()) {
mediaItems = objects;
Collections.sort(mediaItems, new Comparator<BOObject>() {
final Collator c = Collator.getInstance(Locale.GERMAN);
#Override
public int compare(BOObject s1, BOObject s2) {
if (s2 != null && ((BOMedia) s2).getTitle() != null && s1 != null
&& ((BOMedia) s1).getTitle() != null) {
return c.compare(((BOMedia) s1).getTitle(),((BOMedia) s2).getTitle());
} else {
return 0;
}
}
});
displayMedia();
FragmentHelper.hideProgressSpinner(layoutContainer, android.R.id.list);
}
}
#Override
public void onDbCallException(Exception exception) {
if (!getActivity().isFinishing()) {
FragmentHelper.hideProgressSpinner(layoutContainer, android.R.id.list);
}
}
});
}
}
MediaAdapter.java
public class MediaAdapter extends BaseAdapter {
private static final String TAG = MediaAdapter.class.getSimpleName();
private Context context;
private ArrayList<BOObject> mediaItems;
public MediaAdapter(Context c, ArrayList<BOObject> mediaItems) {
super();
context = c;
this.mediaItems = mediaItems;
}
#Override
public int getCount() {
return mediaItems.size();
}
#Override
public Object getItem(int position) {
return null;
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = new MediaItemView(context);
}
((MediaItemView)convertView).initialize((BOMedia) mediaItems.get(position));
return convertView;
}
public void setItems(ArrayList<BOObject> mediaItems) {
this.mediaItems = mediaItems;
}
}
MediaItemView.java
public class MediaItemView extends LinearLayout {
private static final String TAG = MediaItemView.class.getSimpleName();
private BOMedia item;
private SoftReference<Bitmap> bm;
private ImageView iv;
private Context ctx;
public MediaItemView(Context context) {
super(context);
LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
layoutInflater.inflate(R.layout.view_media_item, this);
this.ctx = context;
}
/** Init the view with a new BOMedia object
* #param mediaItem
*/
public void initialize(BOMedia mediaItem) {
this.item = mediaItem;
initUI();
}
private void initUI() {
TextView title = (TextView) findViewById(R.id.itemText);
iv = (ImageView) findViewById(R.id.itemImage);
title.setText(Html.fromHtml(item.getTitle()));
iv.setImageBitmap(null);
bm = null;
System.gc();
iv.invalidate();
if (item.getFilepathThumb() != null && !item.getFilepathThumb().equals("")) {
ExpansionPackManager.getInstance().getBitmapResource(item.getFilepathThumb(), false,
new BitmapReadListener() {
#Override
public void onFileRead(BitmapResponseMessage message) {
Log.d(TAG, "Bitmap read: " + message.getFilepath());
Bitmap image = message.getBitmap();
if (image != null && message.getFilepath().equals(item.getFilepathThumb())) {
bm = new SoftReference<Bitmap>(image);
iv.setImageBitmap(bm.get());
Log.d(TAG, "image set");
} else {
Log.d(TAG, "image too late: " + image);
}
}
#Override
public void onFileException(Throwable exception) {
Log.d(TAG, "image exception");
}
});
}
}
}
In MediaItemView the size of your bitmap must be too big. If the bitmap is 600x600 and you want to display a image with a size of 50x50 you can use Bitmap.createScaledBitmap. You should also use bitmap cache while loading your bitmap.
This is because the View for rach child in the ListView is recreated as you scroll through. This is very heavy on resources. To avoid this use a holder class in adapters getView() to hold and reuse the views. This is called an Efficient Adapter. For example see Efficient List Adapter in API demos. http://developer.android.com/tools/samples/index.html
You can also use:
android:hardwareAccelerated = true
Beginning in Android 3.0 (API level 11), the Android 2D rendering pipeline is designed to better support hardware acceleration. Hardware acceleration carries out all drawing operations that are performed on a View's canvas using the GPU.
For more info http://developer.android.com/guide/topics/graphics/hardware-accel.html