I've spent the day playing around with the GreenDroid PagedView and PagedAdaptor to create multiple screens that one can swipe through (a la iPhone style and Android Home Screen style). It's a very cool effect and the GreenDroid library is awesome. Soo.. I can get this working for one XML layout (code below), but what I am attempting to do is have three screens with different layouts. I've played around and got it working successfully with one method, but am unsure whether the method I'm using will cause problems due to repeat inflation of XML layout at every method call. I'll give some examples of what I've tried to save time. I'm hoping someone reads this that has had experience with GreenDroid and can point me in the right direction. Maybe this will also help someone else too..
Here is my code:
HomePageActivity.java
public class HomePageActivity extends GDActivity {
private static final int PAGE_COUNT = 3;
private final Handler mHandler = new Handler();
private PageIndicator mPageIndicator;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setActionBarContentView(R.layout.homepage_view); // container xml
final PagedView pagedView = (PagedView) findViewById(R.id.homepage_view);
pagedView.setOnPageChangeListener(mOnPagedViewChangedListener);
pagedView.setAdapter(new HomePageAdapter());
mPageIndicator = (PageIndicator) findViewById(R.id.page_indicator);
mPageIndicator.setDotCount(PAGE_COUNT);
setActivePage(pagedView.getCurrentPage());
}
private void setActivePage(int page) {
mPageIndicator.setActiveDot(page);
}
private OnPagedViewChangeListener mOnPagedViewChangedListener = new OnPagedViewChangeListener() {
#Override
public void onStopTracking(PagedView pagedView) {
}
#Override
public void onStartTracking(PagedView pagedView) {
}
#Override
public void onPageChanged(PagedView pagedView, int previousPage, int newPage) {
setActivePage(newPage);
}
};
private class HomePageAdapter extends PagedAdapter {
#Override
public int getCount() {
return PAGE_COUNT;
}
#Override
public Object getItem(int position) {
return null;
}
#Override
public long getItemId(int position) {
return 0;
}
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
Log.i("getView","ConvertView is null");
convertView = getLayoutInflater().inflate(R.layout.homepage_one, parent, false);
}
Log.i("getView Page",Integer.toString(position));
return convertView;
}
}
The two lines:
Log.i("getView","ConvertView is null");
Log.i("getView Page",Integer.toString(position));
I am using to debug this to see what is happening via. LogCat and so I can experiment.
The first line is displayed ONCE - first time application is created. I've noticed that VERY VERY occasionally as I swipe through the pages (I have three - set by variable PAGE_COUNT at top), that after 30-50 swipes I will see this message again. This shows that this layout is only INFLATED once - for all screens.
The second line I naturally see every time I change page. The integer position is either 0, 1 or 2 in this particular case.
The XML layout homepage_one is a simple layout with three TextViews. The XML layout homepage_view is the container for these pages.
homepage_view.xml
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:greendroid="http://schemas.android.com/apk/res/com.cyrilmottier.android.gdcatalog">
<greendroid.widget.PagedView
android:id="#+id/homepage_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#drawable/splash" />
<greendroid.widget.PageIndicator
android:id="#+id/page_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal"
android:paddingTop="10dp" />
Naturally I've tried some obvious methods such as this: (which works as wanted)
public View getView(int position, View convertView, ViewGroup parent) {
if (position == 0) {
convertView = getLayoutInflater().inflate(R.layout.homepage_one, parent, false);
} else if( position == 1 ){
convertView = getLayoutInflater().inflate(R.layout.homepage_two, parent, false);
} else {
convertView = getLayoutInflater().inflate(R.layout.homepage_three, parent, false);
}
return convertView;
}
This works as intended. Every screen is different as I want. I've not noticed any performance problems during testing, but it's worth noting in the original instance that we only inflate the layout ONCE. As this method is called (which is every time the page changes, we inflate the layout again. This doesn't seem resourceful, but then again this is my first week with Android development and I don't know how the architecture works... (just reading, reading, reading and testing, testing, testing) - so hence why I would like to hear from more experienced and knowledgeable developers. This may well be acceptable.. I would like to hear from you.
I tried this:
public View getView(int position, View convertView, ViewGroup parent) {
if (PAGE_ONE == false && position == 0) {
convertView = getLayoutInflater().inflate(R.layout.homepage_one, parent, false);
PAGE_ONE = true;
} else if(PAGE_TWO == false && position == 1 ){
convertView = getLayoutInflater().inflate(R.layout.homepage_two, parent, false);
PAGE_TWO = true;
} else if( PAGE_THREE == false && position == 2 ){
convertView = getLayoutInflater().inflate(R.layout.homepage_three,parent, false);
PAGE_THREE = true;
}
return convertView;
}
PAGE_ONE, PAGE_TWO, PAGE_THREE were set in the class as private boolean = false. The consideration behind this is that we only inflate layout once and every other call to this method simply returns the already inflated xml layout. Unfortunately this crashes my emulator and my phone (HTC EVO 3D). As Jay-Z would say, on to the next one...
Any ideas guys?
The GreenDroid's PagedView is normally intended to display a large set of pages. It can be considered as an horizontal paged ListView. This is the reason why using it is very close to the common ListAdapter.
In your case, you want to keep three pages in memory. This is completely possible but not recommended as you are preventing the PagedView from recycling your pages and also increasing the amount of View in memory.
Having only 3 pages (with a relatively flat and narrow View hierarchy) is OK. A better way do to what you are trying to do would be to lazily-initialize the pages and keep a reference on each pages.
private static final int PAGE_COUNT = 3;
private View[] mPages = new View[PAGE_COUNT];
public View getView(int position, View convertView, ViewGroup parent) {
if (mPages[position] == null) {
View page = createPageAtPosition(position, parent);
mPages[position] = page;
}
return mPages[position];
}
Of course you may have a warning in your logs as the PagedView notify the developer when the convertView is not reused.
I ran into a similar issue as well. Not sure if it is a bug with GreenDroid or user error on my part. (The latter is more likely)
Anyway, here is the solution I came up with. Your solution was nearly there, but you missed the critical 'else' condition when grabbing an existing view. It's working quite well for me.
/** get the view to display */
#Override
public View getView(int position, View convertView, ViewGroup parent) {
pageEnum currentPage = pageEnum.values()[position];
if(null == mViews[position]) {
switch(currentPage) {
case PAGE_1:
convertView = getLayoutInflater().inflate(R.layout.page1, parent, false);
break;
case PAGE_2L:
convertView = getLayoutInflater().inflate(R.layout.page2, parent, false);
break;
case PAGE_3:
convertView = getLayoutInflater().inflate(R.layout.page3, parent, false);
break;
default:
convertView = null;
Log.e(TAG, "Invalid page.");
break;
} /* switch(currentPage) */
mViews[position] = convertView;
} /* if(null == mViews[position]) */
else {
convertView = mViews[position];
}
return convertView;
} /* getView() */
Related
I have a recycler adapter which has 3 itemviewtypes. No.1 type of view has an initial animation to show the view which I've written under the holder instance check in the OnBindView . it works fine. but when there is any change in the other view types, notifydatasetchanged() is called and the adapter is set again which makes the animation start again.
i tried putting a flag for the animation where it would only animate the first time. is there a smarter way to do this?
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int
viewType) {
mSharedPreferencesManager.setVersionNewFeatureCheck(BuildConfig.VERSION_NAME);
Logger.i(TAG, "onCreateViewHolder, " + viewType);
if (viewType == TYPE_HEADER) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.converation_header_row, parent, false);
return new HeaderViewHolder(v);
} else if(viewType == TYPE_ANNOUNCEMENT) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.chat_new_feature_notification, parent, false);
return new FeatureNotificationViewHolder(v);
}
else{
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_row, parent, false);
return new ConversationRowViewHolder(v);
}
}
f (holder instanceof FeatureNotificationViewHolder) {
final FeatureNotificationViewHolder featureNotificationViewHolder = (FeatureNotificationViewHolder) holder;
featureNotificationViewHolder.chatNewFeatureNotif.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
final int height = featureNotificationViewHolder.chatNewFeatureNotif.getMeasuredHeight();
featureNotificationViewHolder.chatNewFeatureNotif.setVisibility(View.GONE);
featureNotificationViewHolder.chatNewFeatureNotif.getViewTreeObserver().removeOnPreDrawListener(this);
mOverflowAnimations.showView(featureNotificationViewHolder.chatNewFeatureNotif, height, 6);
return false;
}
});
.notifyDataSetChanged() will notify all of your items inside the RecyclerView, if you don't want everything to reset consider using .notifyItemChanged(int position) method on your adapter instance. You'll need to know exactly the index/position of the item to be notified.
Instead of using notifyDataSetChanged, you should use notifyItemChanged to only change the item that needs to be changed, and not the whole adapter or data set.
This won't lead to the unnecessary animation because you're only changing a specific item. This is also much faster and takes up less processing and rendering power.
You can read more about it here.
I want my Adapter to choose either one row_layout or another row_layout, depending on which Activity is sending the data. There are two Activities sending data.
At the moment I only know how to make the Adapter use one row_layout. I can't work out how to write the extra code to make it choose a different row_layout, depending on which Activity is sending the data. (There wont be any checkbox in the second row_layout).
Here is my Adapter:
public class ShopItemAdapter extends ArrayAdapter<ShopItem> {
public ShopItemAdapter(Context context, ArrayList<ShopItem> shopItem){
super(context, 0, shopItem);
}
#NonNull
#Override
public View getView(int position, View convertView, #NonNull ViewGroup parent) {
if(convertView == null){
convertView = LayoutInflater.from(getContext()).inflate(R.layout.row_layout, parent, false);
}
TextView tvItem = (TextView)convertView.findViewById(R.id.tvItemName);
TextView tvQty = (TextView)convertView.findViewById(R.id.tvQuantity);
CheckBox cbIsPurchased = (CheckBox)convertView.findViewById(R.id.cbIsPurchased);
ShopItem _shopItem = getItem(position);
tvItem.setText(_shopItem.getItemName());
tvQty.setText(String.valueOf(_shopItem.getQuantity()));
if(_shopItem.getIsPurchased() == 1){
cbIsPurchased.setChecked(true);
}
else{
cbIsPurchased.setChecked(false);
}
return convertView;
}
}
You can add some parameter to the ShopItemAdapter constructor to distinguish from which Activity is called, but if that implies to use many if conditionals in getView code then i think it's better to write several adapters, you will have a much clear code.
I am using ListView with alternate rows colored. But when the ListView exceeds the length of the screen, on scrolling, the alternate color disappears.
Below is my code:
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = getActivity().getLayoutInflater()
.inflate(R.layout.problem_list_row, null);
if(position%2!=0) {
convertView.setBackgroundColor(Color.parseColor("#ebebeb"));
}
}
if(position%2!=0){
convertView.setBackgroundColor(Color.parseColor("#ebebeb"));
}
/* some code */
return convertView;
}
Attached below, the image of the ListView.
Try this code
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = getActivity().getLayoutInflater()
.inflate(R.layout.problem_list_row, null);
}
if(position%2==0)
{
// SET EVEN POSITION COLOR
convertView.setBackgroundColor(Color.parseColor("#ebebeb"));
}
else
{
//SET ODD POSITION COLOR
}
/* some code */
return convertView;
}
I hope this helps you.
convertView is null if the entire ListView fits on the screen. However once you start scrolling, Views that move off the screen get passed to the getView() method as convertView. Sometimes the convertView received will be grey even though you want the View to be white. Your code doesn't deal with this case because it only colours Views grey, it never clears the background colour when it is no longer wanted. To solve this problem you need to set the colour both for odd and even positions.
This should work.
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = getActivity().getLayoutInflater()
.inflate(R.layout.problem_list_row, null);
}
convertView.setBackgroundColor(Color.parseColor(position%2!=0 ? "#ebebeb" : "#ffffff"));
/* some code */
return convertView;
}
I would recommend you to use multiple views, so two .xml file in your case.
I did exactly the same thing you want in an article on my website: http://raverat.github.io/android-listview-multi-views/
Hope this will help!
I created a custom listview layout with images which are loaded from web like this:
http://i.stack.imgur.com/l8ZOc.png
It works fine when scrolling down. However, when you scroll down, the previous items go out of screen then destroyed. When you try to scroll up again, it gets loaded again (from cache, faster but not instant) which causes a delay and it is not fluent as it should be.
1.Is there an example of how to do this properly?
2.Is there a way to prevent listview items being destroyed when they are out of screen?
3.If so, will it cause problems to keep too many items?
Bellow is my code:
MenuAdapter:
public class MenuAdapter extends BaseAdapter{
Context context;
List<MyMenuItem> menuItems;
MenuAdapter(Context context, List<MyMenuItem> menuItems) {
this.context = context;
this.menuItems = menuItems;
}
#Override
public int getCount() {
return menuItems.size();
}
#Override
public Object getItem(int position) {
return menuItems.get(position);
}
#Override
public long getItemId(int position) {
return menuItems.indexOf(getItem(position));
}
private class ViewHolder {
ImageView ivMenu;
TextView tvMenuHeader;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
LayoutInflater mInflater = (LayoutInflater) context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
if (convertView == null) {
convertView = mInflater.inflate(R.layout.menu_item, null);
holder = new ViewHolder();
holder.tvMenuHeader = (TextView) convertView.findViewById(R.id.tvMenuHeader);
holder.ivMenu = (ImageView) convertView.findViewById(R.id.ivMenuItem);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
MyMenuItem row_pos = menuItems.get(position);
Picasso.with(context)
.load(row_pos.getItem_image_url())
.into(holder.ivMenu);
holder.tvMenuHeader.setText(row_pos.getItem_header());
Log.e("Test", "headers:" + row_pos.getItem_header());
return convertView;
}
}
MyMenuItem:
public class MyMenuItem {
private String item_header;
private String item_image_url;
public MyMenuItem(String item_header, String item_image_url){
this.item_header=item_header;
this.item_image_url=item_image_url;
}
public String getItem_header(){
return item_header;
}
public void setItem_header(String item_header){
this.item_header=item_header;
}
public String getItem_image_url(){
return item_image_url;
}
public void setItem_image_url(String item_image_url){
this.item_image_url=item_image_url;
}
}
MainActivity:
public class MyActivity extends Activity implements AdapterView.OnItemClickListener {
List<MyMenuItem> menuItems;
ListView myListView;
JSONArray jsonArray;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
Bundle extras = getIntent().getExtras();
if(extras!=null){
try{
jsonArray = new JSONArray(extras.getString("Data"));
}catch (Exception e){
e.printStackTrace();
}
}
menuItems = new ArrayList<MyMenuItem>();
for (int i = 0; i < jsonArray.length(); i++) {
try {
MyMenuItem item = new MyMenuItem(jsonArray.getJSONObject(i).getString("title"), jsonArray.getJSONObject(i).getString("imageURL"));
menuItems.add(item);
}catch (Exception e){
e.printStackTrace();
}
}
myListView = (ListView) findViewById(R.id.list);
MenuAdapter adapter = new MenuAdapter(this, menuItems);
myListView.setAdapter(adapter);
myListView.setOnItemClickListener(this);
}
}
MenuItem.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/ivMenuItem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="center"
android:src="#drawable/em" />
<TextView
android:id="#+id/tvMenuHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#55000000"
android:paddingBottom="15dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="15dp"
android:textColor="#android:color/white"
android:layout_gravity="left|top"
android:layout_alignBottom="#+id/ivMenuItem"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />
</RelativeLayout>
1. Is there an example of how to do this properly?
Your code looks pretty close to perfect. The Adapter's getView method is usually the critical path to optimize. Compare for example Picasso's own example SampleListDetailAdapter.java. The important points it (as well as your code) does
check for & re-use already inflated views, inflation is expensive.
use ViewHolder so you don't have to call findViewById every time. Not terribly expensive on simple views. Also cached afaik.
Picasso.with(context).load(url)... each time you need to display an image. This should finish instantly but still use caches and other magic.
There are some minor optimizations you can add, but I doubt that there are noticeable or even measurable changes:
pure style change: use BaseAdapter#getItem(position). This method
exists for you only. The framework doesn't use it.
#Override
public MyMenuItem getItem(int position) { // << subclasses can use subtypes in overridden methods!
return menuItems.get(position);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
...
MyMenuItem row_pos = getItem(position);
Use a sane id method
#Override
public long getItemId(int position) {
return menuItems.indexOf(getItem(position));
}
is equivalent to
#Override
public long getItemId(int position) {
return position;
}
but now infinitely faster. indexOf(Object) scales really badly with the number of objects.
Cache objects that don't change:
MenuAdapter(Context context, List<MyMenuItem> menuItems) {
this.mLayoutInflater = LayoutInflater.from(content);
this.mPicasso = Picasso.with(context);
}
..
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.menu_item, null);
...
mPicasso
.load(row_pos.getItem_image_url())
.into(holder.ivMenu);
2. Is there a way to prevent listview items being destroyed when they are out of screen?
No(*).
..(*) well you can essentially cache the result of getView e.g. in LruCache(position, View) or LruCache(MyMenuItem, View), then don't touch the convertView - they need to remain unconverted or you would kill those views in your cache. Also
#Override
public int getItemViewType(int position) {
return Adapter.IGNORE_ITEM_VIEW_TYPE;
}
seemed to be required because the standard adapter using code assumes that views it removes from visibility are gone. They are not and messing with them messes with your cache and caused weird display problems for me.
3. If so, will it cause problems to keep too many items?
Yes. This behavior is not intendend / expected. There is also more or less nothing you gain. You might be able to save you the call to holder.tvMenuHeader.setText(). Likewise the one to Picasso but both of them should complete instantly. Picasso should have your image cached already. By caching all Views you essentially add another cache that also contains all the images. I would rather check that the picasso cache works as intended and holds most items. The only reason you may want to do it with view caching is for cases that require complicated setup of the view, so it becomes worth caching the completely constructed view rather than just some content parts.
Profile
Profiling can actually tell you where you can / need / should improve. The first to look at IMO is traceview. You'll see if code blocks the main thread which results in choppy list scrolling. If you're doing complicated views and you see that the draw methods are executed most of the time, profile them as well.
http://www.curious-creature.org/docs/android-performance-case-study-1.html
http://blog.venmo.com/hf2t3h4x98p5e13z82pl8j66ngcmry/performance-tuning-on-android
http://www.vogella.com/tutorials/AndroidTools/article.html
I'm trying to fix an issues that I've seen previously and fixed on iOS, but am unable to fix on android. In iOS, I used the SDWebImage library to download and cache images. However, when scrolling through a long list of cells, the images would come up in the wrong cells. I was able ot fix this by doing the following:
#property (weak) id <SDWebImageOperation> imageOperation;
...
- (void)setFriend:(TAGUser *)friend {
...
self.imageOperation = [[SDWebImageManager sharedManager] downloadWithURL:[NSURL URLWithString:friend.profileImageUrl] options:SDWebImageRetryFailed
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
if(!error) {
self.profileImageView.image = image;
}
}];
...
}
- (void)prepareForReuse {
[super prepareForReuse];
if (self.imageOperation) {
[self.imageOperation cancel];
}
self.imageOperation = nil;
}
On Android, I am using the Picaso Libray and trying to achieve the same result like so:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View vi = convertView;
if (vi == null) {
vi = inflater.inflate(R.layout.friend_row, parent, false);
ViewHolder holder = new ViewHolder();
holder.friendImage = (ImageView) vi.findViewById(R.id.friend_image);
}
final ViewHolder holder = (ViewHolder) vi.getTag(); //Bad naming convention in my project I know, but it's a built-in method
//THIS SHOULD IN THEORY CANCEL THE REQUEST OF THE OLD FRIEND URL
Picasso.with(mActivity.getBaseContext()).cancelRequest(holder.friendImage);
holder.friendImage.setImageDrawable(mActivity.getResources().getDrawable(R.drawable.background_circle));
final TagUser user = (TagUser) getItem(position);
Picasso.with(mActivity.getBaseContext()).load(user.getProfileUrl()).transform(new RoundedTransformation(ViewUtility.convertPxToDp(mActivity, 23), 0)).fit().into(holder.friendImage,
new Callback() {
#Override
public void onSuccess() {
holder.friendInitials.setVisibility(View.INVISIBLE);
}
#Override
public void onError() {
holder.friendInitials.setVisibility(View.VISIBLE);
}
});
}
Even with cancelRequeset getting called, the profile images are still getting mismatched. Any help would be greatly appreciated!
You don't need to call cancel. Picasso automatically sees when views are being re-used and will cancel old downloads for that image view.
I would also recommend using Picasso's .placeholder API for the background circle.
You seem to be missing a call to setTag when the layout is inflated. Hopefully that's just an error in copying to the post.
Lastly, create the RoundedTransformation once and re-use the same instance for all calls to Picasso.
In the end your code should look like this:
private final Transformation roundTransform;
// Set the following in constructor:
// roundTransform = new RoundedTransformation(ViewUtility.convertPxToDp(mActivity, 23), 0)
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View vi = convertView;
if (vi == null) {
vi = inflater.inflate(R.layout.friend_row, parent, false);
ViewHolder holder = new ViewHolder();
vi.setTag(holder)
holder.friendImage = (ImageView) vi.findViewById(R.id.friend_image);
}
final ViewHolder holder = (ViewHolder) vi.getTag(); //Bad naming convention in my project I know, but it's a built-in method
final TagUser user = (TagUser) getItem(position);
Picasso.with(mActivity)
.load(user.getProfileUrl())
.placeholder(R.drawable.background_circle)
.transform(roundTransform)
.fit()
.into(holder.friendImage,
new Callback() {
#Override
public void onSuccess() {
holder.friendInitials.setVisibility(View.INVISIBLE);
}
#Override
public void onError() {
holder.friendInitials.setVisibility(View.VISIBLE);
}
});
}
I would advice to use Volley by Google. I have used it in many projects and it never gave mismatched results even in case I had nearly 1000 images in 3 column gridView. Volley