this is my activity
i want to insert a native ads into the list view.
I'm trying to follow this guide https://github.com/StartApp-SDK/Documentation/wiki/android-advanced-usage But I find it hard to understand.
can you give me a hand, maybe making examples of code? thank you
ACTIVITY
public class EpisodiActivity extends Activity {
private StartAppAd startAppAd = new StartAppAd(this);
public class ViewModel {
private String url;
private String name;
public ViewModel(String url, String name) {
this.url = url;
this.name = name;
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return this.name;
}
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// creazione fullscreen activity
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.episodi_activity);
String[] episodi = getIntent().getStringArrayExtra("Product");
String[] urls = getIntent().getStringArrayExtra("urls");
ListView mylist = (ListView) findViewById(R.id.listView1);
// And in this loop we create the ViewModel instances from
// the name and url and add them all to a List
List<ViewModel> models = new ArrayList<ViewModel>();
for (int i = 0; i < episodi.length; i++) {
String name = episodi[i];
String url = "No value";
if (i < urls.length) {
url = urls[i];
}
ViewModel model = new ViewModel(url, name);
models.add(model);
}
// Here we create the ArrayAdapter and assign it to the ListView
// We pass the List of ViewModel instances into the ArrayAdapter
final ArrayAdapter<ViewModel> adapter = new ArrayAdapter<ViewModel>(
this, android.R.layout.simple_list_item_1, models);
mylist.setAdapter(adapter);
mylist.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> arg0, View v, int position,
long id) {
// Here we get the ViewModel at the given position
ViewModel model = (ViewModel) arg0.getItemAtPosition(position);
// And the url from the ViewModel
String url = model.getUrl();
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
});
}
#Override
public void onResume() {
super.onResume();
startAppAd.onResume();
startAppAd.showAd();
}
#Override
public void onPause() {
super.onPause();
startAppAd.onPause();
}
}
A listView BaseAdapter first calls the method getCount() to retrieve the number of rows to show and then for each row it calls the getView(int position, View convertView, ViewGroup parent) method to create and return a View object for the given position.
The approach is to extend BaseAdapter and create your own custom Adapter, instead of using the default ArrayAdapter. Then you need to return an “Ad” View instead of your usual normal View when you want to display an ad. This means that you need to override getCount() to return more rows (for example if you have 10 rows, that means you need to return 11 = 10 actual content + 1 ad)
Then you need to decide in which position to create this View, I think you can do it by simply checking the position variable:
if (position == VALUE) {
// Create and return Ad View
} else {
// Create and return a normal View
}
Anyway, this whole thing is really tricky as things can go easily out of hand (positions mismatching with Views etc). StartApp should be able to control your listView adapter to do all this for you. My guess is that your adapter is not communicating properly with StartApp (maybe you are not initialising correctly?).
Try to dig into the documentation or find an example by them. If you can’t figure it out, there are other alternatives you can use such as Avocarrot, Namomedia, Inmobi, etc
I have used Avocarrot which has an open source example in github for inserting ads in listViews.
You can run it and use it if it fits you: https://github.com/Avocarrot/android-demo-app
Recently I stucked with the same question but for a new Admob native ads. Then I decided to post my solution for that to admobadapter. Hope it will help you. I believe you could use the AdmobAdapterWrapper after some customizations for the StartApp-SDK...Kindly look at the AdmobFetcher.java. The result could appear like this
I've been working on this for a while now. I'm using a BaseAdapter and a LinearLayout holder for the ad. These are my global variables:
LinearLayout adHolderView;
private AdRequest adRequest;
NativeExpressAdView singleAdView;
I also have a few other global variables:
public static boolean adLoaded;
public static boolean allowShowAd = true;
public static int adLocation = 5;
Point adSanityCheck = new Point(0,0);
Here is my BaseAdapter, please read the comments
public BaseAdapter drawingsGridAdapter = new BaseAdapter() {
//viewHolder classes are very common with BaseAdapters to speed up loading
//It is not exactly needed but will be very usefull for different uses.
//For example if you are using a GridView instead of a ListView like I am in this case
//it would be good to have the orientation String which is used later on
//in the adapter
class ViewHolderItem {
ImageView imageView;
ImageView userImageView;
TextView userNameView;
TextView dateView;
TextView numCommentsView;
ImageView starView;
TextView starCount;
//below items are used for ads in a gridView
int orientation;
final int LANDSCAPE = 1;
final int PORTRAIT = 2;
}
#SuppressLint("SimpleDateFormat")
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolderItem viewHolder;
//check if you can use your viewHolderItem and make sure that
//the view is not an Adview
if (convertView == null || !(convertView.getTag() instanceof ViewHolderItem)) {
viewHolder = new ViewHolderItem();
LinearLayout ll = (LinearLayout) inflater.inflate(R.layout.gallery_item, null);
viewHolder.dateView = (TextView) ll.findViewById(R.id.gal_item_date);
//...
//initialize your viewHolder items here
//...
convertView = ll;
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolderItem) convertView.getTag();
}
//...
Tools.addDateToTextView(viewHolder.dateView, drawings.get(position).time);
//do your view modifications here
//...
//now we start doing the ad stuff
//this is optional. add it in to make it work on a gridview
boolean landscape = false; if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
landscape = true;
//in my case I wanted the ad to be the same size as all my other Items so I made sure the values are accesable.
//since you are only using a ListView, you only really need to check for getMeasuredHeight() or you could define your own
//AdSize
if (convertView.getMeasuredHeight()>0 && convertView.getMeasuredWidth()>0 && allowShowAd) {
int adH = convertView.getMeasuredHeight();
int adW = convertView.getMeasuredWidth();
//if the adview needs to be set up then it will set it up, else it will make sure that the ad is the correct size
//and fix it if it needs to
if (singleAdView == null){
if ((landscape && viewHolder.orientation == viewHolder.LANDSCAPE)
||(!landscape && viewHolder.orientation == viewHolder.PORTRAIT))
setUpAd(adW,adH, landscape);
}else{
if ((landscape && viewHolder.orientation == viewHolder.LANDSCAPE)
||(!landscape && viewHolder.orientation == viewHolder.PORTRAIT)) {
//Log.d("ads","Adsize"+singleAdView.getAdSize());
if (singleAdView.getAdSize().getHeight() !=Tools.convertPixelsToDp(adH-1,MainMenuActivity.this)
|| singleAdView.getAdSize().getWidth() !=Tools.convertPixelsToDp(adW-1,MainMenuActivity.this)) {
Log.d("Ads","ad sizesW:"+singleAdView.getAdSize().getWidth() + "vs" + Tools.convertPixelsToDp(adW-1,MainMenuActivity.this));
Log.d("Ads","ad sizesH:"+singleAdView.getAdSize().getHeight() + "vs" + Tools.convertPixelsToDp(adH-1,MainMenuActivity.this));
//sometimes the correct size is not reported so this will ensure ads are not loaded twice. This only really needs to be checked with a gridview
if (adSanityCheck.x == adW && adSanityCheck.y == adH) {
setUpAd(adW, adH, landscape);
}else{
adSanityCheck.x = adW;
adSanityCheck.y = adH;
}
}
}
}
}//Log.d("Ads","tag ="+av.getTag());
if (position == adLocation && singleAdView!=null && allowShowAd) {
if (adLoaded) {
adHolderView.postDelayed(new Runnable() {
#Override
public void run() {
//this seemed to help avoid ad flicker. may want to test to make sure
if (singleAdView!=null)
singleAdView.requestLayout();
}
}, 100);
return adHolderView;
}
}
if (landscape)
viewHolder.orientation = viewHolder.LANDSCAPE;
else
viewHolder.orientation = viewHolder.PORTRAIT;
return convertView;
}#Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return 0;
}
#Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return null;
}
#Override
public int getViewTypeCount() {
return 2;
}
#Override
public int getItemViewType(int position) {
if (position == adLocation && singleAdView!=null && adLoaded && allowShowAd){
return 1;
}
return 0;
}
#Override
public int getCount() {
return drawings.size();
}
};
You will notice in my BaseAdapter, that I have a method called setUpAd:
private void setUpAd(int widthPX, int heightPX, boolean isLandscape){
if (allowShowAd) {
Log.d("draw", "setupAD");
destroyAdView();
adHolderView = new LinearLayout(MainMenuActivity.this);
singleAdView = new NativeExpressAdView(this);
adLoaded = false;
drawingsGridAdapter.notifyDataSetChanged();
singleAdView.setId(R.id.googleIdentify);
singleAdView.setAdUnitId("ca-app-pub-xxxxxxxxxxxxxx/xxxxxxxxxxxxxx");
int wh = Tools.convertPixelsToDp(widthPX - 1, MainMenuActivity.this);
singleAdView.setAdSize(new AdSize(Tools.convertPixelsToDp(widthPX - 1, MainMenuActivity.this),
Tools.convertPixelsToDp(heightPX - 1, MainMenuActivity.this)));
adRequest = new AdRequest.Builder()
.addTestDevice("TEST DEV ID")
.build();
singleAdView.setAdListener(new AdListener() {
#Override
public void onAdFailedToLoad(int errorCode) {
super.onAdFailedToLoad(errorCode);
adLoaded = false;
drawingsGridAdapter.notifyDataSetChanged();
}
#Override
public void onAdLoaded() {
super.onAdLoaded();
adLoaded = true;
drawingsGridAdapter.notifyDataSetChanged();
}
});
singleAdView.loadAd(adRequest);
adHolderView.setLayoutParams(new LinearLayout.LayoutParams(widthPX, heightPX));
adHolderView.setBackgroundColor(Color.WHITE);
adHolderView.addView(singleAdView);
}
}
From that you need a few other methods:
private void destroyAdView()
{
if (singleAdView != null)
{
adRequest = null;
singleAdView.removeAllViews();
singleAdView.setAdListener(null);
singleAdView.destroy();
singleAdView.setEnabled(false);
adHolderView.removeView(singleAdView);
singleAdView = null;
adHolderView = null;
}
}
public static int convertPixelsToDp(float px, Context context){
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
float dp = px / ((float)metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
return (int) dp;
}
And Finally, If you end up using a GridView, here is one last thing to add in order to load the right size ad after orientation changes:
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
allowShowAd = false;
gridView.postDelayed(new Runnable() {
#Override
public void run() {
drawingsGridAdapter.notifyDataSetChanged();
gridView.invalidateViews();
gridView.requestLayout();
gridView.postDelayed(new Runnable() {
#Override
public void run() {
drawingsGridAdapter.notifyDataSetChanged();
}
},500);
gridView.postDelayed(new Runnable() {
#Override
public void run() {
allowShowAd = true;
Log.d("Ads","allow ads now");
drawingsGridAdapter.notifyDataSetChanged();
}
},1500);
}
}, 100);
}
I do not know the StartApp framework, but basically you have to do the following:
Write your own adapter and do not use ArrayAdapter. Here is a basis class you can use to simplify the adapters view recycling: https://github.com/sockeqwe/appkit/blob/master/adapter/src/main/java/com/hannesdorfmann/appkit/adapter/SimpleAdapter.java
Specify own View cells by overriding getItemViewType(int position) and getViewTypeCount()
Specify a own view type and view holder for your banners that will appear in the ListView.
From what I have seen, the banner needs to save something in onSaveInstanceState() and to restore something in onRestoreInstanceState() . Im not sure if this is needed or makes sence in a ListView. Simply try it with or without this calls. If this is needed, than you have to keep a List of Banner Items in you adapter and you
have to do something like this in your Activities code:
public class MyActivity extends Activity
{
public void onSaveInstanceState(Bundle b){
super.onSaveInstanceState(b);
adapter.saveInstancState(b);
}
public void restoreInstanceState(Bundle b){
super.restoreInstanceState(b);
adapter.restoreInstanceState(b);
}
}
Related
I am using parse-server to develop the app which uses RecyclerView to display image items.
but the problem is that the items displayed in the view changed every time I scrolled up and down.
I want to know what is the problem on my code.
if you see below images, you can find the items are changing their position.
I tried to make holder image become null before call the holder again. but it's not working. I guess that the item's position number is changed when I call the item again.but I can't find the cause of the situation
enter image description here
enter image description here
RecyclerParseAdapter.java
public class MyTimelineAdapter extends RecyclerParseAdapter {
private interface OnQueryLoadListener<ParseObject> {
public void onLoading();
public void onLoaded(List<ParseObject> objects, Exception e);
}
private static ParseQueryAdapter.QueryFactory<ParseObject> queryFactory;
private static List<OnQueryLoadListener<ParseObject>> onQueryLoadListeners;
private static List<List<ParseObject>> objectPages;
private static ArrayList<ParseObject> items;
private static int currentPage;
private static RequestManager requestManager;
public MyTimelineAdapter(Context context, RequestManager requestManager) {
super(context);
this.requestManager = requestManager;
this.onQueryLoadListeners = new ArrayList<>();
this.currentPage = 0;
this.objectPages = new ArrayList<>();
this.items = new ArrayList<>();
this.queryFactory = new ParseQueryAdapter.QueryFactory<ParseObject>() {
#Override
public ParseQuery<ParseObject> create() {
ParseQuery<ParseObject> query = ParseQuery.getQuery("ImageClassName");
query.setCachePolicy(ParseQuery.CachePolicy.CACHE_THEN_NETWORK);
query.whereEqualTo("status", true);
query.orderByDescending("createdAt");
return query;
}
};
loadObjects(currentPage);
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View timelineView;
timelineView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_timeline_item2, parent, false);
TimelineItemViewHolder timelineItemViewHolder = new TimelineItemViewHolder(timelineView);
return timelineItemViewHolder;
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final ParseObject timelineOb = getItem(position);
FunctionPost functionPost = new FunctionPost(context);
functionPost.TimelineArtistPostAdapterBuilder( timelineOb, holder, requestManager);
//기능 추가
}
#Override
public int getItemCount() {
return items.size();
}
#Override
public ParseObject getItem(int position) {
return items.get(position);
}
#Override
public void loadObjects(final int page) {
final ParseQuery<ParseObject> query = this.queryFactory.create();
if (this.objectsPerPage > 0 && this.paginationEnabled) {
this.setPageOnQuery(page, query);
}
this.notifyOnLoadingListeners();
if (page >= objectPages.size()) {
objectPages.add(page, new ArrayList<ParseObject>());
}
query.findInBackground(new FindCallback<ParseObject>() {
#Override
public void done(List<ParseObject> foundObjects, ParseException e) {
if ((e != null) && ((e.getCode() == ParseException.CONNECTION_FAILED) || (e.getCode() != ParseException.CACHE_MISS))) {
hasNextPage = true;
} else if (foundObjects != null) {
// Only advance the page, this prevents second call back from CACHE_THEN_NETWORK to
// reset the page.
if (page >= currentPage) {
currentPage = page;
// since we set limit == objectsPerPage + 1
hasNextPage = (foundObjects.size() > objectsPerPage);
}
if (paginationEnabled && foundObjects.size() > objectsPerPage) {
// Remove the last object, fetched in order to tell us whether there was a "next page"
foundObjects.remove(objectsPerPage);
}
List<ParseObject> currentPage = objectPages.get(page);
currentPage.clear();
currentPage.addAll(foundObjects);
syncObjectsWithPages(items, objectPages);
// executes on the UI thread
notifyDataSetChanged();
}
notifyOnLoadedListeners(foundObjects, e);
}
});
}
public void loadNextPage() {
if (items.size() == 0) {
loadObjects(0);
} else {
loadObjects(currentPage + 1);
}
}
public void syncObjectsWithPages(ArrayList<ParseObject> items, List<List<ParseObject>> objectPages) {
items.clear();
for (List<ParseObject> pageOfObjects : objectPages) {
items.addAll(pageOfObjects);
}
}
protected void setPageOnQuery(int page, ParseQuery<ParseObject> query) {
query.setLimit(this.objectsPerPage + 1);
query.setSkip(page * this.objectsPerPage);
}
public void addOnQueryLoadListener(OnQueryLoadListener<ParseObject> listener) {
this.onQueryLoadListeners.add(listener);
}
public void removeOnQueryLoadListener(OnQueryLoadListener<ParseObject> listener) {
this.onQueryLoadListeners.remove(listener);
}
public void notifyOnLoadingListeners() {
for (OnQueryLoadListener<ParseObject> listener : this.onQueryLoadListeners) {
listener.onLoading();
}
}
public void notifyOnLoadedListeners(List<ParseObject> objects, Exception e) {
for (OnQueryLoadListener<ParseObject> listener : this.onQueryLoadListeners) {
listener.onLoaded(objects, e);
}
}
}
I did find the problem
I add overide method in the adapter then It works find.
#Override
public int getItemViewType(int position) {
return position;
}
I am not sure why it happens now. any one help me to know the cause of problem?
I has a similar problem the other day see this post. onBindViewHolder needs to know how to display the row when it's called. I returned two different view types depending on the need in getItemViewType, inflated the view type conditionally in onCreateViewHolder, then I was able to set the data on the ViewHolder as needed.
I'm working on an app that displays a working schedule on a time line.
This is a rough layout of how the app is designed at the moment:
The data is stored in an SQLite DB. When the Timeline (a singleton object) requests the data from the database helper class, it gets an ArrayList of Events (e.g. an Event could be a duty starting at the 1st of May 2016 at 03:00 and ending at the 3rd of May 2016 at 16:00). The Timeline then transforms these Events to TimelineItems, a class representing (part of) an Event for a particular day.
The loading of Events and the transformation of Events to TimelineItems both are done in AsyncTasks. So far so good.
Now comes the part I'm struggling with: updating the UI after a new DB fetch.
My first approach was to pass the updated ArrayList of TimelineItems to the RecyclerView adapter and let the the adapter know the data has changed with notifyDatasetChanged(). The problem with this approach is that
1) a lot of unnecessary work is being done (cause we're recalculating all Events/TimelineItems, not only the ones changed) and
2) the scroll position on the RecyclerView is reset after every DB fetch
In my 2nd approach, I've implemented some methods to check which Events/TimelineItems have changed since the last display with the idea of only changing those TimelineItems, with notifyItemChanged(). Less work is being done and no need to worry about scroll positions at all. The tricky bit is that checking which items have changed does take some time, so it needs to be done async as well:
I tried to do the code manipulations in doInBackground() and the UI updating by posting otto bus events in onProgressUpdate().
private class InsertEventsTask extends AsyncTask<Void, Integer, Void> {
#Override
protected Void doInBackground(Void... params) {
ArrayList<Event> events = mCachedEvents;
// if mChangedEvents is not null and not empty
if (events != null && !events.isEmpty()) {
// get the list of pairs for the events
ArrayList<TimelineItemForDateTimePair> listOfPairs = convertEventsToPairs(events);
// insert the TimelineItems from the pairs into the Timeline
for (int i = 0; i < listOfPairs.size(); i++) {
// get the last position for the DateTime associated with the pair
int position = findLastPositionForDate(listOfPairs.get(i).dateTime);
// if position is -1, the events started on a day before the timeline starts
// so keep skipping pairs until position > -1
if (position > -1) {
// if the item is a PlaceholderItem
if (mTimelineItems.get(position).isPlaceholderItem) {
// remove the PlaceholderItem
mTimelineItems.remove(position);
// and add the TimelineItem from the pair at the position the PlaceholderItem was at
mTimelineItems.add(position, listOfPairs.get(i).timelineItem);
// update the UI on the UI thread
publishProgress(position, TYPE_CHANGED);
} else { // if the item is not a PlaceholderItem, there's already an normal TimelineItem in place
// place the new item at the next position on the Timeline
mTimelineItems.add(position + 1, listOfPairs.get(i).timelineItem);
publishProgress(position, TYPE_ADDED);
}
}
}
}
return null;
}
/**
* onProgressUpdate handles the UI changes on the UI thread for us. Type int available:
* - TYPE_CHANGED
* - TYPE_ADDED
* - TYPE_DELETED
*
* #param values value[0] is the position as <code>int</code>,
* value[1] is the type of manipulation as <code>int</code>
*/
#Override
protected void onProgressUpdate(Integer... values) {
int position = values[0];
int type = values[1];
// update the UI for each changed/added/deleted TimelineItem
if (type == TYPE_CHANGED) {
BusProvider.getInstance().post(new TimelineItemChangedNotification(position));
} else if (type == TYPE_ADDED) {
BusProvider.getInstance().post((new TimelineItemAddedNotification(position)));
} else if (type == TYPE_DELETED) {
// TODO: make delete work bro!
}
}
}
The problem is, that somehow, scrolling while this progress is being posted messes up the UI completely.
My main problem is: when I update a specific item in the data set (TimelineItems) of the adapter, notifyItemChanged() does change the item but doesn't put the item at the correct position.
Here's my adapter:
/**
* A custom RecyclerView Adapter to display a Timeline in a TimelineFragment.
*/
public class TimelineAdapter extends RecyclerView.Adapter<TimelineAdapter.TimelineItemViewHolder> {
/*************
* VARIABLES *
*************/
private ArrayList<TimelineItem> mTimelineItems;
/****************
* CONSTRUCTORS *
****************/
/**
* Constructor with <code>ArrayList<TimelineItem></code> as data set argument.
*
* #param timelineItems ArrayList with TimelineItems to display
*/
public TimelineAdapter(ArrayList<TimelineItem> timelineItems) {
this.mTimelineItems = timelineItems;
}
// Create new views (invoked by the layout manager)
#Override
public TimelineItemViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
// create a new view
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_timeline, parent, false);
// set the view's size, margins, paddings and layout parameters
// ...
return new TimelineItemViewHolder(v);
}
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(TimelineItemViewHolder holder, int position) {
// - get element from your data set at this position
// - replace the contents of the view with that element
// if the item is a ShowPreviousMonthsItem, set the showPreviousMonthsText accordingly
if (mTimelineItems.get(position).isShowPreviousMonthsItem) {
holder.showPreviousMonthsText.setText(mTimelineItems.get(position).showPreviousMonthsText);
} else { // otherwise set the showPreviousMonthsText blank
holder.showPreviousMonthsText.setText("");
}
// day of month & day of week of the TimelineItem
if (mTimelineItems.get(position).isFirstItemOfDay) {
holder.dayOfWeek.setText(mTimelineItems.get(position).dayOfWeek);
holder.dayOfMonth.setText(mTimelineItems.get(position).dayOfMonth);
} else {
holder.dayOfWeek.setText("");
holder.dayOfMonth.setText("");
}
// Event name for the TimelineItem
holder.name.setText(mTimelineItems.get(position).name);
// place and goingTo of the TimelineItem
// if combinedPlace == ""
if(mTimelineItems.get(position).combinedPlace.equals("")) {
if (mTimelineItems.get(position).isFirstDayOfEvent) {
holder.place.setText(mTimelineItems.get(position).place);
} else {
holder.place.setText("");
}
if (mTimelineItems.get(position).isLastDayOfEvent) {
holder.goingTo.setText(mTimelineItems.get(position).goingTo);
} else {
holder.goingTo.setText("");
}
holder.combinedPlace.setText("");
} else {
holder.place.setText("");
holder.goingTo.setText("");
holder.combinedPlace.setText(mTimelineItems.get(position).combinedPlace);
}
if(mTimelineItems.get(position).startDateTime != null) {
holder.startTime.setText(mTimelineItems.get(position).startDateTime.toString("HH:mm"));
} else {
holder.startTime.setText("");
}
if(mTimelineItems.get(position).endDateTime != null) {
holder.endTime.setText(mTimelineItems.get(position).endDateTime.toString("HH:mm"));
} else {
holder.endTime.setText("");
}
if (!mTimelineItems.get(position).isShowPreviousMonthsItem) {
if (mTimelineItems.get(position).date.getDayOfWeek() == DateTimeConstants.SUNDAY) {
holder.dayOfWeek.setTextColor(Color.RED);
holder.dayOfMonth.setTextColor(Color.RED);
} else {
holder.dayOfWeek.setTypeface(null, Typeface.NORMAL);
holder.dayOfMonth.setTypeface(null, Typeface.NORMAL);
holder.dayOfWeek.setTextColor(Color.GRAY);
holder.dayOfMonth.setTextColor(Color.GRAY);
}
} else {
((RelativeLayout) holder.dayOfWeek.getParent()).setBackgroundColor(Color.WHITE);
}
holder.bindTimelineItem(mTimelineItems.get(position));
}
// Return the size of the data set (invoked by the layout manager)
#Override
public int getItemCount() {
return mTimelineItems.size();
}
// replace the data set
public void setTimelineItems(ArrayList<TimelineItem> timelineItems) {
this.mTimelineItems = timelineItems;
}
// replace an item in the data set
public void swapTimelineItemAtPosition(TimelineItem item, int position) {
mTimelineItems.remove(position);
mTimelineItems.add(position, item);
notifyItemChanged(position);
}
// the ViewHolder class containing the relevant views,
// also binds the Timeline item itself to handle onClick events
public class TimelineItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
protected TextView dayOfWeek;
protected TextView dayOfMonth;
protected TextView showPreviousMonthsText;
protected TextView name;
protected TextView place;
protected TextView combinedPlace;
protected TextView goingTo;
protected TextView startTime;
protected TextView endTime;
protected TimelineItem timelineItem;
public TimelineItemViewHolder(View view) {
super(view);
view.setOnClickListener(this);
this.dayOfWeek = (TextView) view.findViewById(R.id.day_of_week);
this.dayOfMonth = (TextView) view.findViewById(R.id.day_of_month);
this.showPreviousMonthsText = (TextView) view.findViewById(R.id.load_previous_data);
this.name = (TextView) view.findViewById(R.id.name);
this.place = (TextView) view.findViewById(R.id.place);
this.combinedPlace = (TextView) view.findViewById(R.id.combined_place);
this.goingTo = (TextView) view.findViewById(R.id.going_to);
this.startTime = (TextView) view.findViewById(R.id.start_time);
this.endTime = (TextView) view.findViewById(R.id.end_time);
}
public void bindTimelineItem(TimelineItem item) {
timelineItem = item;
}
// handles the onClick of a TimelineItem
#Override
public void onClick(View v) {
// if the TimelineItem is a ShowPreviousMonthsItem
if (timelineItem.isShowPreviousMonthsItem) {
BusProvider.getInstance().post(new ShowPreviousMonthsRequest());
}
// if the TimelineItem is a PlaceholderItem
else if (timelineItem.isPlaceholderItem) {
Toast.makeText(v.getContext(), "(no details)", Toast.LENGTH_SHORT).show();
}
// else the TimelineItem is an actual event
else {
Toast.makeText(v.getContext(), "eventId = " + timelineItem.eventId, Toast.LENGTH_SHORT).show();
}
}
}
And this is the method that is triggered in the TimelineFragment when a change is posted on the event bus:
#Subscribe
public void onTimelineItemChanged(TimelineItemChangedNotification notification) {
int position = notification.position;
Log.d(TAG, "TimelineItemChanged detected for position " + position);
mAdapter.swapTimelineItemAtPosition(mTimeline.mTimelineItems.get(position), position);
mAdapter.notifyItemChanged(position);
Log.d(TAG, "Item for position " + position + " swapped");
}
A thing to note is that the data set of the adapter seems to display correctly after I scrolled away from the changed data far enough and return to the position after that. Initially the UI is totally messed up though.
EDIT:
I found that adding
mAdapter.notifyItemRangeChanged(position, mAdapter.getItemCount());
resolves the issue but - unfortunately - sets the scroll position to the one being changed :(
Here's my TimelineFragment:
/**
* Fragment displaying a Timeline using a RecyclerView
*/
public class TimelineFragment extends BackHandledFragment {
// DEBUG flag and TAG
private static final boolean DEBUG = false;
private static final String TAG = TimelineFragment.class.getSimpleName();
// variables
protected RecyclerView mRecyclerView;
protected TimelineAdapter mAdapter;
protected LinearLayoutManager mLinearLayoutManager;
protected Timeline mTimeline;
protected MenuItem mMenuItemScroll2Today;
protected MenuItem mMenuItemReload;
protected String mToolbarTitle;
// TODO: get the value of this boolean from the shared preferences
private boolean mUseTimelineItemDividers = true;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// get a handle to the app's Timeline singleton
mTimeline = Timeline.getInstance();
setHasOptionsMenu(true);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
rootView.setTag(TAG);
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.timeline_list);
mRecyclerView.hasFixedSize();
// LinearLayoutManager constructor
mLinearLayoutManager = new LinearLayoutManager(getActivity());
// set the layout manager
setRecyclerViewLayoutManager();
// adapter constructor
mAdapter = new TimelineAdapter(mTimeline.mTimelineItems);
// set the adapter for the RecyclerView.
mRecyclerView.setAdapter(mAdapter);
// add lines between the different items if using them
if (mUseTimelineItemDividers) {
RecyclerView.ItemDecoration itemDecoration =
new TimelineItemDivider(this.getContext());
mRecyclerView.addItemDecoration(itemDecoration);
}
// add the onScrollListener
mRecyclerView.addOnScrollListener(new TimelineOnScrollListener(mLinearLayoutManager) {
// when the first visible item on the Timeline changes,
// adjust the Toolbar title accordingly
#Override
public void onFirstVisibleItemChanged(int position) {
mTimeline.mCurrentScrollPosition = position;
try {
String title = mTimeline.mTimelineItems
.get(position).date
.toString(TimelineConfig.TOOLBAR_DATE_FORMAT);
// if mToolbarTitle is null, set it to the new title and post on bus
if (mToolbarTitle == null) {
if (DEBUG)
Log.d(TAG, "mToolbarTitle is null - posting new title request on bus: " + title);
mToolbarTitle = title;
BusProvider.getInstance().post(new ChangeToolbarTitleRequest(mToolbarTitle));
} else { // if mToolbarTitle is not null
// only post on the bus if the new title is different from the previous one
if (!title.equals(mToolbarTitle)) {
if (DEBUG)
Log.d(TAG, "mToolbarTitle is NOT null, but new title detected - posting new title request on bus: " + title);
mToolbarTitle = title;
BusProvider.getInstance().post(new ChangeToolbarTitleRequest(mToolbarTitle));
}
}
} catch (NullPointerException e) {
// if the onFirstVisibleItemChanged is called on a "ShowPreviousMonthsItem",
// leave the title as it is
}
}
});
return rootView;
}
/**
* Set RecyclerView's LayoutManager to the one given.
*/
public void setRecyclerViewLayoutManager() {
int scrollPosition;
// If a layout manager has already been set, get current scroll position.
if (mRecyclerView.getLayoutManager() != null) {
scrollPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager())
.findFirstCompletelyVisibleItemPosition();
} else {
scrollPosition = mTimeline.mFirstPositionForToday;
}
mRecyclerView.setLayoutManager(mLinearLayoutManager);
mLinearLayoutManager.scrollToPositionWithOffset(scrollPosition, 0);
}
// set additional menu items for the Timeline fragment
#Override
public void onPrepareOptionsMenu(Menu menu) {
// scroll to today
mMenuItemScroll2Today = menu.findItem(R.id.action_scroll2today);
mMenuItemScroll2Today.setVisible(true);
mMenuItemScroll2Today.setIcon(Timeline.getIconForDateTime(new DateTime()));
mMenuItemScroll2Today.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
#Override
public boolean onMenuItemClick(MenuItem item) {
// stop scrolling
mRecyclerView.stopScroll();
// get today's position
int todaysPosition = mTimeline.mFirstPositionForToday;
// scroll to today's position
mLinearLayoutManager.scrollToPositionWithOffset(todaysPosition, 0);
return false;
}
});
// reload data from Hacklberry
mMenuItemReload = menu.findItem(R.id.action_reload_from_hacklberry);
mMenuItemReload.setVisible(true);
mMenuItemReload.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
#Override
public boolean onMenuItemClick(MenuItem item) {
// stop scrolling
mRecyclerView.stopScroll();
//
mTimeline.reloadDBForCurrentMonth();
mTimeline.loadEventsFromUninfinityDBAsync(mTimeline.mTimelineStart, mTimeline.mTimelineEnd);
return false;
}
});
super.onPrepareOptionsMenu(menu);
}
#Override
public void onResume() {
super.onResume();
// if the Timeline has been invalidated, let AllInOneActivity know it needs to replace
// this Fragment with a new one
if (mTimeline.isInvalidated()) {
Log.d(TAG, "posting TimelineInvalidatedNotification on the bus ...");
BusProvider.getInstance().post(
new TimelineInvalidatedNotification());
}
// fetch today's menu icon
if (mMenuItemScroll2Today != null) {
if (DEBUG) Log.d(TAG, "fetching scroll2today menu icon");
mMenuItemScroll2Today.setIcon(Timeline.getIconForDateTime(new DateTime()));
}
}
// from BackHandledFragment
#Override
public String getTagText() {
return TAG;
}
// from BackHandledFragment
#Override
public boolean onBackPressed() {
return false;
}
#Subscribe
public void onHacklberryReloaded(HacklberryLoadedNotification notification) {
resetReloading();
}
// handles ShowPreviousMonthsRequests posted on the bus by the TimelineAdapter's ShowPreviousMonthsItem onClick()
#Subscribe
public void onShowPreviousMonthsRequest(ShowPreviousMonthsRequest request) {
// create an empty OnItemTouchListener to prevent the user from manipulating
// the RecyclerView while it loads more data (would mess up the scroll position)
EmptyOnItemTouchListener listener = new EmptyOnItemTouchListener();
// add it to the RecyclerView
mRecyclerView.addOnItemTouchListener(listener);
// load the previous months (= add the required TimelineItems)
int newScrollToPosition = mTimeline.showPreviousMonths();
// pass the new data set to the TimelineAdapter
mAdapter.setTimelineItems(mTimeline.mTimelineItems);
// notify the adapter the data set has changed
mAdapter.notifyDataSetChanged();
// scroll to the last scroll (updated) position
mLinearLayoutManager.scrollToPositionWithOffset(newScrollToPosition, 0);
}
#Subscribe
public void onTimelineItemChanged(TimelineItemChangeNotification notification) {
int position = notification.position;
Log.d(TAG, "TimelineItemChanged detected for position " + position);
mAdapter.swapTimelineItemAtPosition(mTimeline.mTimelineItems.get(position), position);
//mAdapter.notifyItemRangeChanged(position, position);
Log.d(TAG, "Item for position " + position + " swapped");
}
I've taken a screenshot of the app after it first loads. I'll explain real quick what happens on initialisation:
the Timeline is built by populating all days with PlaceholderItems (a TimelineItem with just a Date).
Events are loaded from the DB and transformed to TimelineItems
Whenever a new TimelineItem has changed and is ready, the Timeline pokes the TimelineFragment via the otto bus to update the data set of the adapter for that particular position with the new TimelineItem.
Here's a screenshot of what happens after the initial load:
the Timeline is loaded but certain items are inserted at the wrong position.
When scrolling away and returning to the range of days that was displayed incorrectly before, all is good:
About your second approach. Probably your code is not workind because you have Data Race on mTimelineItems and mCachedEvents. I can't see all of your code, but it seems that you using mTimelineItems inside doInBackground() simultaneously with the UI thread without any synchronization.
I propose you to make a mix of your first and second approaches:
Make a copy of the original data (mTimelineItems) and send it to the AsyncTask.
Change the copy asynchronously in doInBackground() and log all changes.
Return the changed data and logs to the UI thread.
Apply the new data to the RecyclerView by using logs.
Let me illustrate this approach in code.
Data management:
public class AsyncDataUpdater
{
/**
* Example data entity. We will use it
* in our RecyclerView.
*/
public static class TimelineItem
{
public final String name;
public final float value;
public TimelineItem(String name, float value)
{
this.name = name;
this.value = value;
}
}
/**
* That's how we will apply our data changes
* on the RecyclerView.
*/
public static class Diff
{
// 0 - ADD; 1 - CHANGE; 2 - REMOVE;
final int command;
final int position;
Diff(int command, int position)
{
this.command = command;
this.position = position;
}
}
/**
* And that's how we will notify the RecyclerView
* about changes.
*/
public interface DataChangeListener
{
void onDataChanged(ArrayList<Diff> diffs);
}
private static class TaskResult
{
final ArrayList<Diff> diffs;
final ArrayList<TimelineItem> items;
TaskResult(ArrayList<TimelineItem> items, ArrayList<Diff> diffs)
{
this.diffs = diffs;
this.items = items;
}
}
private class InsertEventsTask extends AsyncTask<Void, Void, TaskResult>
{
//NOTE: this is copy of the original data.
private ArrayList<TimelineItem> _old_items;
InsertEventsTask(ArrayList<TimelineItem> items)
{
_old_items = items;
}
#Override
protected TaskResult doInBackground(Void... params)
{
ArrayList<Diff> diffs = new ArrayList<>();
try
{
//TODO: long operation(Database, network, ...).
Thread.sleep(1000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
//Some crazy manipulation with data...
//NOTE: we change the copy of the original data!
Random rand = new Random();
for(int i = 0; i < 10; i ++)
{
float rnd = rand.nextFloat() * 100.0f;
for(int j = 0; j < _old_items.size(); j++)
{
if(_old_items.get(j).value > rnd)
{
TimelineItem item = new TimelineItem("Item " + rnd, rnd);
//Change data.
_old_items.add(j, item);
//Log the changes.
diffs.add(new Diff(0, j));
break;
}
}
}
for(int i = 0; i < 5; i ++)
{
int rnd_index = rand.nextInt(_old_items.size());
//Change data.
_old_items.remove(rnd_index);
//Log the changes.
diffs.add(new Diff(2, rnd_index));
}
//...
return new TaskResult(_old_items, diffs);
}
#Override
protected void onPostExecute(TaskResult result)
{
super.onPostExecute(result);
//Apply the new data in the UI thread.
_items = result.items;
if(_listener != null)
_listener.onDataChanged(result.diffs);
}
}
private DataChangeListener _listener;
private InsertEventsTask _task = null;
/** Managed data. */
private ArrayList<TimelineItem> _items = new ArrayList<>();
public AsyncDataUpdater()
{
// Some test data.
for(float i = 10.0f; i <= 100.0f; i += 10.0f)
_items.add(new TimelineItem("Item " + i, i));
}
public void setDataChangeListener(DataChangeListener listener)
{
_listener = listener;
}
public void updateDataAsync()
{
if(_task != null)
_task.cancel(true);
// NOTE: we should to make the new copy of the _items array.
_task = new InsertEventsTask(new ArrayList<>(_items));
_task.execute();
}
public int getItemsCount()
{
return _items.size();
}
public TimelineItem getItem(int index)
{
return _items.get(index);
}
}
Using in UI:
public class MainActivity extends AppCompatActivity
{
private static class ViewHolder extends RecyclerView.ViewHolder
{
private final TextView name;
private final ProgressBar value;
ViewHolder(View itemView)
{
super(itemView);
name = (TextView)itemView.findViewById(R.id.tv_name);
value = (ProgressBar)itemView.findViewById(R.id.pb_value);
}
void bind(AsyncDataUpdater.TimelineItem item)
{
name.setText(item.name);
value.setProgress((int)item.value);
}
}
private static class Adapter extends RecyclerView.Adapter<ViewHolder>
implements AsyncDataUpdater.DataChangeListener
{
private final AsyncDataUpdater _data;
Adapter(AsyncDataUpdater data)
{
_data = data;
_data.setDataChangeListener(this);
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item, parent, false);
return new ViewHolder(v);
}
#Override
public void onBindViewHolder(ViewHolder holder, int position)
{
holder.bind(_data.getItem(position));
}
#Override
public int getItemCount()
{
return _data.getItemsCount();
}
#Override
public void onDataChanged(ArrayList<AsyncDataUpdater.Diff> diffs)
{
//Apply changes.
for(AsyncDataUpdater.Diff d : diffs)
{
if(d.command == 0)
notifyItemInserted(d.position);
else if(d.command == 1)
notifyItemChanged(d.position);
else if(d.command == 2)
notifyItemRemoved(d.position);
}
}
}
private AsyncDataUpdater _data = new AsyncDataUpdater();
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView rv_content = (RecyclerView)findViewById(R.id.rv_content);
rv_content.setLayoutManager(new LinearLayoutManager(this));
rv_content.setAdapter(new Adapter(_data));
Button btn_add = (Button)findViewById(R.id.btn_add);
btn_add.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
_data.updateDataAsync();
}
});
}
}
I put Example application on GH, so you can test it if you want.
Update 1
About Data Race.
this.mTimelineItems = timelineItems; inside TimelineAdapter() constructor makes a copy of the reference to the ArrayList, but not the copy of the ArrayList itself. So you have two references: TimelineAdapter.mTimelineItems and Timeline.mTimelineItems, that both refer to the same ArrayList object. Please, look at this.
The data race occurs when doInBackground() called from Worker Thread and onProgressUpdate() called from UI Thread simultaneously. The main reason is that publishProgress() does not call onProgressUpdate() synchronously. Instead, publishProgress() plans the call of onProgressUpdate() on UI Thread in the future. Here is a good description of the problem.
Off topic.
This:
mTimelineItems.set(position, item);
should be faster than this:
mTimelineItems.remove(position);
mTimelineItems.add(position, item);
i have been trying to implement the endless scroll feature for my product listing page of a ecommmerce app. The grids show the details of the items and when i scroll to the bottom i need to show a progress bar and then append the new grid of items.
The api call works like this, I need to send a start_row_number and limit, which will send me all the items from the start_row_number to limit. Example: start_row_number = 0 and limit = 10. This will return items from 0 to 10
After that i need to load more items when the user reaches the bottom of the grid, and append it to the gridview. So i will send start_row_number = 10 and limit = 10, this will return items form 10 to 20.
As of now, i can get the items from 0 to 10 but not after that. How can i create the endless scroll feature and make everything such that it doesn't give me error such as 'too much work on the main thread'
Here is my MainActivity:
public class ProductListing extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.product_listing_act);
init();
}
public void productListingApiCall(ProductListingCondtionModel productListingCondtionModel) {
RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint(productListingCondtionModel.getBase_url()).setLogLevel(RestAdapter.LogLevel.FULL).build();
final ProductListingApi productListingApi =
restAdapter.create(ProductListingApi.class);
productListingApi.getFeed(productListingCondtionModel.getFile(),
productListingCondtionModel.getOperation_condition(),
productListingCondtionModel.getSearch_string_condition(),
productListingCondtionModel.getMinprice_condition(),
productListingCondtionModel.getMaxprice_condition(),
productListingCondtionModel.getMincusratings_condition(),
productListingCondtionModel.getMaxcusratings_condition(),
productListingCondtionModel.getDiscount_condition(),
productListingCondtionModel.getCatids_condition(),
productListingCondtionModel.getBrands_condition(),
productListingCondtionModel.getAffids_condition(),
productListingCondtionModel.getStart_row_condition(),
productListingCondtionModel.getLimit(),
productListingCondtionModel.getOrderby_condition(),
productListingCondtionModel.getSortby_condition(), new Callback<ProductListingPojo>() {
#Override
public void success(ProductListingPojo productListingPojo, Response response) {
final ProductListingPojo product = productListingPojo;
new Thread(new Runnable() {
#Override
public void run() {
String[] t = Arrays.copyOf(product.getTitle(),
product.getTitle().length);
int[] p = Arrays.copyOf(product.getSellingprice(),
product.getSellingprice().length);
int[] m = Arrays.copyOf(product.getMrp(),
product.getMrp().length);
int[] d = Arrays.copyOf(product.getDiscountpercent(),
product.getDiscountpercent().length);
String[] i = Arrays.copyOf(product.getProductimageSmall1(),
product.getProductimageSmall1().length);
for(int j = 0; j < t.length; j++) {
CategoryAllApi categoryAllApi = new CategoryAllApi();
categoryAllApi.setTitle(t[j]);
categoryAllApi.setPrice(p[j]);
categoryAllApi.setMrp(m[j]);
categoryAllApi.setDiscount(d[j]);
categoryAllApi.setImage(i[j]);
arrayList.add(categoryAllApi);
}
}
}).run();
setAdapter();
}
#Override
public void failure(RetrofitError error) {
tv_title_header.setText(error.getMessage());
Log.e("error", error.getMessage());
}
});
}
void setAdapter() {
adapter = new ProductListingGridAdapter(this, arrayList);
gv_product_listing_act.setAdapter(adapter);
}
}
Heres the Adapter:
public class ProductListingGridAdapter extends BaseAdapter {
public ProductListingGridAdapter(ProductListing productListing, ArrayList<CategoryAllApi> arrayList) {
this.arrayList= arrayList;
context = productListing;
inflater = ( LayoutInflater )context.
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#Override
public int getCount() {
return arrayList.size();
}
#Override
public Object getItem(int position) {
return position;
}
#Override
public long getItemId(int position) {
return position;
}
public class Holder
{
ImageView im_pic;
TextView tv_title, tv_price, tv_mrp, tv_discount;
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
GridView grid = (GridView) parent;
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
int width = metrics.widthPixels;
grid.setColumnWidth(width);
grid.setNumColumns(2);
int size = grid.getRequestedColumnWidth() / 2 ;
Double d = new Double(size * 2);
int h = d.intValue();
Holder holder = new Holder();
View rowView;
int index = grid.getFirstVisiblePosition();
View v = grid.getChildAt(0);
int top = (v == null) ? 0 : (v.getTop() - grid.getPaddingTop());
grid.setSelectionFromTop(index, top);
rowView = inflater.inflate(R.layout.product_listing_gv_items_lay, null);
rowView.setLayoutParams(new GridView.LayoutParams(size, h));
holder.im_pic = (ImageView) rowView.findViewById(R.id.im_product_listing_gv_items_lay_pic);
holder.tv_title = (TextView) rowView.findViewById(R.id.tv_product_listing_gv_items_lay_title);
holder.tv_price = (TextView) rowView.findViewById(R.id.tv_product_listing_gv_items_lay_price);
holder.tv_mrp = (TextView) rowView.findViewById(R.id.tv_product_listing_gv_items_lay_mrp);
holder.tv_discount = (TextView) rowView.findViewById(R.id.tv_product_listing_gv_items_lay_discount);
holder.tv_title.setTypeface(EasyFonts.robotoMedium(rowView.getContext()));
holder.tv_price.setTypeface(EasyFonts.robotoBlack(rowView.getContext()));
holder.tv_mrp.setTypeface(EasyFonts.robotoLight(rowView.getContext()));
holder.tv_mrp.setPaintFlags(holder.tv_mrp.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
holder.tv_discount.setTypeface(EasyFonts.robotoLight(rowView.getContext()));
categoryAllApi = arrayList.get(position);
Ion.with(holder.im_pic).load(categoryAllApi.getImage());
holder.tv_title.setText(categoryAllApi.getTitle());
holder.tv_price.setText("Rs. " + categoryAllApi.getPrice());
holder.tv_mrp.setText("Rs. " + categoryAllApi.getMrp());
holder.tv_discount.setText("" + categoryAllApi.getDiscount() + "%");
rowView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent intent = new Intent(context, ProductDetails.class);
Bundle bundle = new Bundle();
bundle.putString("operation", "");
bundle.putString("productkey", "");
intent.putExtras(bundle);
context.startActivity(intent);
}
});
return rowView;
}
}
Heres the CategoryApiCall.java:
public class CategoryAllApi {
private String title, image;
private int price, mrp, discount;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getMrp() {
return mrp;
}
public void setMrp(int mrp) {
this.mrp = mrp;
}
public int getDiscount() {
return discount;
}
public void setDiscount(int discount) {
this.discount = discount;
}
}
I see you have used a GridView.
RecyclerView has been introduced by Google and overcomes flaws of listview and Gridview which filled the ram with junk and made app clunky.
Use RecyclerView with GridLayoutManager. Also, research endless scroll in RecyclerView (which is pretty easy to implement).
I strongly recommend this for your app, since I have tried the same and result is outstanding. App is faster, ram becomes light, and scrolling is great. Also there are many features like the recyclerView object will offer. About time Google took care of such things and raise app quality.
Process may look like a task but in long run it will help you and the app users a lot.
I have a ListView in which there several rows containing two buttons and a ProgressBar (Visibility:GONE) each.
My purpose is to display the ProgressBar upon click on the buttons and after completing a certain set of background operations remove that row entirely.
The problem here is that after removing the item from the ArrayList which the ListView is created upon and calling notifyDataSetChanged the row is removed successfully but the ProgressBar remains visible.
Shouldn't it be removed along with it's parent view?
Checkout the following record to see the problem in action.
Here is the source of my entire fragment:
public class FriendRequestFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "FriendRequestFragment";
ArrayList<FriendRequest> friendRequests;
#InjectView(R.id.friendRequestList)
ListView mListView;
#InjectView(R.id.noRequestsText)
TextView noRequestsText;
#InjectView(R.id.swipe)
SwipeRefreshLayout swipeRefreshLayout;
// NotificationHandler nh;
/**
* The Adapter which will be used to populate the ListView/GridView with
* Views.
*/
private FriendRequestAdapter mAdapter;
private Context c;
private boolean isProcessing = false;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public FriendRequestFragment() {
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Util.trackFragment(this);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_friendrequest_list, container, false);
ButterKnife.inject(this, view);
c = getActivity();
friendRequests = new ArrayList<>();
swipeRefreshLayout.setOnRefreshListener(this);
mAdapter = new FriendRequestAdapter(getActivity(), friendRequests);
mListView.setAdapter(mAdapter);
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
int topRowVerticalPosition =
(view == null || view.getChildCount() == 0) ?
0 : view.getChildAt(0).getTop();
swipeRefreshLayout.setEnabled(firstVisibleItem == 0 && topRowVerticalPosition >= 0);
Log.d(TAG, "SwipeRefresh: " + String.valueOf(firstVisibleItem == 0 && topRowVerticalPosition >= 0));
}
});
loadRequests();
return view;
}
private void loadRequests() {
// nh = new NotificationHandler(getActivity());
swipeRefreshLayout.setRefreshing(true);
Log.d(TAG, "loading requests init");
HashMap<String, Integer> params = new HashMap<>();
params.put("profile_id", Util.getCurrentProfileID(c));
final String uniqueID = Util.getCurrentProfileID(c) + String.valueOf(System.currentTimeMillis() / 1000 / 1200);
new ApiRequest(Util.URL_GET_FRIEND_REQUESTS, params, new AjaxCallback<String>() {
#Override
public void callback(String url, String result, AjaxStatus status) {
super.callback(url, result, status);
ApiResponse apiResponse = new ApiResponse(url, result, uniqueID);
Log.d(TAG, "Friend Requests Response: " + result);
if (apiResponse.isSuccessful()) {
JSONArray jsonArray = apiResponse.getDataJSONArray();
try {
for (int i = 0; i < jsonArray.length(); i++) {
friendRequests.add(new FriendRequest(jsonArray.getJSONObject(i)));
}
mAdapter.notifyDataSetChanged();
} catch (JSONException e) {
e.printStackTrace();
}
mListView.setVisibility(View.VISIBLE);
} else if (apiResponse.getErrorMessage().equals("request_not_found")) {
noRequestsText.setVisibility(View.VISIBLE);
}
swipeRefreshLayout.setRefreshing(true);
}
}).setUniqueID(uniqueID).execute();
}
#Override
public void onRefresh() {
loadRequests();
}
private void acceptRequest(final int position, final View rootView) {
if (isProcessing) {
CustomToast.makeToast(getActivity(), CustomToast.TYPE_ALERT, getString(R.string.please_wait), CustomToast.LENGTH_SHORT);
return;
}
rootView.findViewById(R.id.loading).setVisibility(View.VISIBLE);
rootView.findViewById(R.id.acceptBtn).setVisibility(View.GONE);
rootView.findViewById(R.id.denyBtn).setVisibility(View.GONE);
isProcessing = true;
Log.d("FriendRequest", "accepting:" + position);
FriendRequest request = friendRequests.get(position);
HashMap<String, Integer> params = new HashMap<>();
params.put("request_id", request.getRequestID());
params.put("profile_id", ProfilesSingleton.getInstance().getCurrentProfile().getProfileID());
new ApiRequest(Util.URL_ACCEPT_REQUEST, params, new AjaxCallback<String>() {
#Override
public void callback(String url, String object, AjaxStatus status) {
super.callback(url, object, status);
ApiResponse apiResponse = new ApiResponse(object);
if (apiResponse.isSuccessful()) {
friendRequests.remove(position);
CustomToast.makeToast(getActivity(), CustomToast.TYPE_DEFAULT,
getString(R.string.you_are_now_friends_with) + " " + friendRequests.get(position).getFullName(),
CustomToast.LENGTH_SHORT);
mAdapter.notifyDataSetChanged();
}else {
rootView.findViewById(R.id.loading).setVisibility(View.GONE);
rootView.findViewById(R.id.acceptBtn).setVisibility(View.VISIBLE);
rootView.findViewById(R.id.denyBtn).setVisibility(View.VISIBLE);
}
isProcessing = false;
}
}).execute();
}
private void denyRequest(final int position, final View rootView) {
if (isProcessing) {
CustomToast.makeToast(getActivity(), CustomToast.TYPE_ALERT, getString(R.string.please_wait), CustomToast.LENGTH_SHORT);
return;
}
rootView.findViewById(R.id.loading).setVisibility(View.VISIBLE);
rootView.findViewById(R.id.acceptBtn).setVisibility(View.GONE);
rootView.findViewById(R.id.denyBtn).setVisibility(View.GONE);
Log.d("FriendRequest", "denying:" + position);
FriendRequest request = friendRequests.get(position);
HashMap<String, Integer> params = new HashMap<>();
params.put("request_id", request.getRequestID());
params.put("profile_id", ProfilesSingleton.getInstance().getCurrentProfile().getProfileID());
new ApiRequest(Util.URL_DENY_REQUEST, params, new AjaxCallback<String>() {
#Override
public void callback(String url, String object, AjaxStatus status) {
super.callback(url, object, status);
ApiResponse apiResponse = new ApiResponse(object);
if (apiResponse.isSuccessful()) {
friendRequests.remove(position);
mAdapter.notifyDataSetChanged();
}else {
rootView.findViewById(R.id.loading).setVisibility(View.GONE);
rootView.findViewById(R.id.acceptBtn).setVisibility(View.VISIBLE);
rootView.findViewById(R.id.denyBtn).setVisibility(View.VISIBLE);
}
}
}).execute();
}
public class FriendRequestAdapter extends ArrayAdapter<FriendRequest> {
public FriendRequestAdapter(Context context, ArrayList<FriendRequest> objects) {
super(context, 0, objects);
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View rootView = convertView;
final ViewHolder holder;
final FriendRequest friendRequest = getItem(position);
if (rootView == null) {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
rootView = inflater.inflate(R.layout.friend_request_item, parent, false);
holder = new ViewHolder();
holder.profilePhoto = (RoundedImageView) rootView.findViewById(R.id.profilePhoto);
holder.fullName = (TextView) rootView.findViewById(R.id.fullName);
holder.acceptBtn = (ImageView) rootView.findViewById(R.id.acceptBtn);
holder.denyBtn = (ImageView) rootView.findViewById(R.id.denyBtn);
holder.loading = (ProgressBar) rootView.findViewById(R.id.loading);
rootView.setTag(holder);
} else {
holder = (ViewHolder) rootView.getTag();
}
holder.fullName.setText(friendRequest.getFullName());
if (friendRequest.getFullPhotoPath().equals("")) {
ImageUtil.replaceWithInitialsView(getContext(), holder.profilePhoto, friendRequest.getInitials());
} else {
Util.aQuery.id(holder.profilePhoto).image(friendRequest.getFullPhotoPath(), false, true, 50, R.drawable.avatar_profile, null, AQuery.FADE_IN);
}
final View finalRootView = rootView;
holder.acceptBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
acceptRequest(position, finalRootView);
}
});
holder.denyBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
denyRequest(position, finalRootView);
}
});
return rootView;
}
public class ViewHolder {
RoundedImageView profilePhoto;
TextView fullName;
ImageView acceptBtn, denyBtn;
ProgressBar loading;
}
}
}
Add a field in your FriendRequest class that saves the current state of the progress bar. based on it set the visibility of the progress bar.
The same view row has been sent to another row. in your getView method you must always set the progress bar visibility based on its status.
Code Sample:
final View finalRootView = rootView;
if (friendRequest.acceptingRequestInProgress())
holder.loading.setVisibility(View.Visibile);
else
holder.loading.setVisibility(View.Gone);
holder.acceptBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
friendRequest.setAcceptingInProgress(true);
acceptRequest(position, finalRootView);
}
});
holder.denyBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
denyRequest(position, finalRootView);
}
});
Another place to modify:
if (apiResponse.isSuccessful()) {
friendRequest.setAcceptingInProgress(false);
friendRequests.remove(position);
mAdapter.notifyDataSetChanged();
}
Note: this is also handles the case when the user scrolls the list
view and the row view in progress is no longer visible. this will
hands the view to another row. But since we check the row state the
progress bar will be stopped. and when user scrolls back to the row
view in progress and hands it a reusable view the progress bar will be
visible again if accepting is still in progress.
Views are getting reused by the ListView and in the getView() method you are not cleaning up the reused view, that's why the progress bar will become visible for an item that shouldn't display it.
Similarly if an item would be removed some items with progress bars visible would loose their progress bar, handing them over to an item that didn't need it.
In getView(), after initializing the holder, you should check if progress bar is necessary.
Start with storing progress bar values at the beginning:
private ArrayList<Integer> progresses = new ArrayList<Integer>();
Update these values every time the list changes (when list changes in loadRequests and when value changes not sure where).
And in getView()
if (progresses.get(position) == 100) {
holder.loading.setVisibility(View.GONE);
} else {
holder.loading.setVisibility(View.VISIBLE);
holder.loading.setProgress(progresses.get(position));
}
The problem is due to visibility of progressbar is VISIBLE default so in getView() after you call notifyDataSetChanged(), the progressbar becomes visible to row position (i - 1).
holder.loading = (ProgressBar) rootView.findViewById(R.id.loading);
holder.loading.setVisibility(View.GONE);
Set progressbar visibility to GONE in getView() and this problem will not come
I use a custom ParseQueryAdapter to load data in listview. I want to show a message when there is no data but the message is shown even when data are not empty. I think it is due to the fact that data are not yet loaded. I tried with setEmptyView and also with a test on the adapter if mAdapter.isEmpty().
I tried waiting a few seconds before testing if adapter is empty but although it works, I think it's not a good practice.
My custom adapter where I make the query:
public class CategoryEventsAdapter extends ParseQueryAdapter<Event> {
public CategoryEventsAdapter(Context context, final String c) {
super(context, new ParseQueryAdapter.QueryFactory<Event>() {
public ParseQuery<Event> create() {
ParseQuery<Event> query = new ParseQuery<Event>("Event");
query.whereEqualTo("published", true);
query.whereEqualTo("category", c);
return query;
}
});
}
#Override
public View getItemView(Event event, View v, ViewGroup parent) {
...
}
}
And I simply call it in a Fragment:
mAdapter = new CategoryEventsAdapter(getActivity(), category);
listview.setAdapter(mAdapter);
if (mAdapter.isEmpty()) {
// show message
}
I've never used this particular part of Parse but looking at the docs, the query seems to be async. Instead you can try this maybe:
mAdapter = new CategoryEventsAdapter(getActivity(), category);
// add a listener for when the query is done.
mAdapter.addOnQueryLoadListener(new OnQueryLoadListener<ParseObject>() {
public void onLoaded(List<ParseObject> objects, ParseException e) {
// Check if empty here and show message.
if (objects.size == 0){
// show message
}
}
});
listview.setAdapter(mAdapter);
So once the query is done, it should call onLoaded so then you can determine if it is empty or not. In onLoaded you can check the count of the objects parameter. Not sure if it's already set in the adapter if you do mAdapter.isEmpty at that point.
This is my ParseQueryAdapter implementation that lets you choose an "empty" placeholder. You just call adapter.setEmptyLayoutId(R.layout.empty) from outside.
public class ParseAdapter extends ParseQueryAdapter {
private final static int EMPTY_VIEW = 2;
private int emptyViewLayoutId;
private boolean isEmpty;
public ParseAdapter(Context c, QueryFactory<? extends ParseObject> q) {
super(c, q);
addOnQueryLoadListener(new OnQueryLoadListener() {
#Override
public void onLoading() {
isEmpty = false;
}
#Override
public void onLoaded(List list, Exception e) {
if (list == null || list.size() == 0) {
isEmpty = true;
}
}
});
}
#Override
public void notifyDataSetChanged() {
isEmpty = false;
super.notifyDataSetChanged();
}
public void setEmptyLayoutId(int emptyViewLayoutId) {
this.emptyViewLayoutId = emptyViewLayoutId;
}
#Override
public View getItemView(ParseObject object, View v, ViewGroup parent) {
if (isEmpty) {
v = View.inflate(getContext(), emptyViewLayoutId, null);
return v;
}
v = v != null ? v : View.inflate(getContext(), rowLayoutId, null);
//do whatever you want on v
return v;
}
#Override
public int getViewTypeCount() {
return super.getViewTypeCount() + 1; //3
}
#Override
public int getItemViewType(int position) {
return isEmpty ? EMPTY_VIEW : super.getItemViewType(position);
}
#Override
public int getCount() {
return isEmpty ? 1 : super.getCount();
}
}
You need to add an item view type because otherwise, if empty, getItemView() won't be called in some cases. Overriding getCount() to return 1 is not enough, because ParseQueryAdapter performs some checks over the view type inside getView(), and won't pass the call to getItemView().