ListView: convertView / holder getting confused - android

I'm working with a ListView, trying to get the convertView / referenceHolder optimisation to work properly but it's giving me trouble. (This is the system where you store the R.id.xxx pointers in as a tag for each View to avoid having to call findViewById). I have a ListView populated with simple rows of an ImageView and some text, but the ImageView can be formatted either for portrait-sized images (tall and narrow) or landscape-sized images (short and wide). It's adjusting this formatting for each row which isn't working as I had hoped.
The basic system is that to begin with, it inflates the layout for each row and sets the ImageView's settings based on the data, and includes an int denoting the orientation in the tag containing the R.id.xxx values. Then when it starts reusing convertViews, it checks this saved orientation against the orientation of the new row. The theory then is that if the orientation is the same, then the ImageView should already be set up correctly. If it isn't, then it sets the parameters for the ImageView as appropriate and updates the tag.
However, I found that it was somehow getting confused; sometimes the tag would get out of sync with the orientation of the ImageView. For example, the tag would still say portrait, but the actual ImageView would still be in landscape layout. I couldn't find a pattern to how or when this happened; it wasn't consistent by orientation, position in the list or speed of scrolling. I can solve the problem by simply removing the check about convertView's orientation and simply always set the ImageView's parameters, but that seems to defeat the purpose of this optimisation.
Can anyone see what I've done wrong in the code below?
static LinearLayout.LayoutParams layoutParams;
(...)
public View getView(int position, View convertView, ViewGroup parent){
ReferenceHolder holder;
if (convertView == null){
convertView = inflater.inflate(R.layout.pick_image_row, null);
holder = new ReferenceHolder();
holder.getIdsAndSetTag(convertView, position);
if (data[position][ORIENTATION] == LANDSCAPE) {
// Layout defaults to portrait settings, so ImageView size needs adjusting.
// layoutParams is modified here, with specific values for width, height, margins etc
holder.image.setLayoutParams(layoutParams);
}
holder.orientation = data[position][ORIENTATION];
} else {
holder = (ReferenceHolder) convertView.getTag();
if (holder.orientation != data[position][ORIENTATION]){ //This is the key if statement for my question
switch (image[position][ORIENTATION]) {
case PORTRAIT:
// layoutParams is reset to the Portrait settings
holder.orientation = data[position][ORIENTATION];
break;
case LANDSCAPE:
// layoutParams is reset to the Landscape settings
holder.orientation = data[position][ORIENTATION];
break;
}
holder.image.setLayoutParams(layoutParams);
}
}
// and the row's image and text is set here, using holder.image.xxx
// and holder.text.xxx
return convertView;
}
static class ReferenceHolder {
ImageView image;
TextView text;
int orientation;
void getIdsAndSetTag(View v, int position){
image = (ImageView) v.findViewById(R.id.pickImageImage);
text = (TextView) v.findViewById(R.id.pickImageText);
orientation = data[position][ORIENTATION];
v.setTag(this);
}
}
Thanks!

Rather than putting orientation as a data member of ReferenceHolder, examine the actual LayoutParams of the ImageView to see what orientation it is in. This way, by definition, you can't get out of sync somehow.
To be honest, I'm confused by the code you have there, as you never seem to change layoutParams, which would seem to be kinda important. Or, shouldn't you have layoutParamsPortrait and layoutParamsLandscape or something? To me, it looks like the rules are:
If it's portrait and the row is initially created, leave it portrait
Everything else is landscape, regardless of what the orientation flag says, since you always set it to layoutParams, which is presumably landscape

Related

RecyclerView Adapter - Where can you access item view after Layout?

onBindViewHolder
Is a nice method but there is one problem - The View has not necessarily been measured yet. So where can I adjust things like amount of content in TextView's etc if I cannot get the actual measurements on the View? I want to change dynamically change the length of Strings rendered in the Item view if the ItemView is a certain width in comparison to the string length. I have measured the CharSet length etc. No problem, but how do I know if it is too long if I cannot measure the width of the View? with the items played out. The String can also be between two items etc. So I need to at least know where I can access this kind of information. Thanks.
You can add a listener to listen for changes in the view tree and get the view's width and height after it has finished measurement.
final ViewTreeObserver obs = mTextView.getViewTreeObserver();
obs.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw () {
int height = mTextView.getHeight();
int width = mTextView.getWidth();
// Return true to proceed with the current drawing pass, or false to cancel.
return true;
}
});

initialize RelativeLayout in this getView, programmatic view generation

I am using a getView in an adapter where I am creating an imageview and making that equal to convertView where the view has already been initialized before. It contains image thumbnails, some of which represent videos.
#Override
public View getView(int position, View convertView, ViewGroup container) {
// First check if this is the top row
if (position < mNumColumns) {
if (convertView == null) {
convertView = new View(mContext);
}
// Set empty view with height of ActionBar
//convertView.setLayoutParams(new AbsListView.LayoutParams(
// LayoutParams.MATCH_PARENT, mActionBarHeight));
return convertView;
}
// Now handle the main ImageView thumbnails
ImageView imageView;
if (convertView == null) { // if it's not recycled, instantiate and initialize
imageView = new RecyclingImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setLayoutParams(mImageViewLayoutParams);
} else { // Otherwise re-use the converted view
imageView = (ImageView) convertView;
}
// Check the height matches our calculated column width
if (imageView.getLayoutParams().height != mItemHeight) {
imageView.setLayoutParams(mImageViewLayoutParams);
}
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
if(images.get(position - mNumColumns).getUriString().contains("video")){
//display video icon
}
else
{
//don't display video icon
}
// Finally load the image asynchronously into the ImageView, this also takes care of
// setting a placeholder image while the background thread runs
if (images != null && !images.isEmpty())
mImageFetcher.loadImage(images.get(position - mNumColumns).getUriString()/*.imageUrls[position - mNumColumns]*/, imageView);
return imageView;
}
The thumbnails do not have a "play" button on them to designate that they are videos, so in those cases I need to add a play button, programmatically.
Typically I use a viewholder pattern with an inflated layout, I am not doing that in this case because I actually don't want some things in memory.
So instead I want to programmatically make a RelativeLayout as the root view of each cell (mRelativeLayout = (RelativeLayout)convertView) and add the imageview and playbutton imageview into that convertview
How do I do that? It requires modification of this statement but I'm not sure how to initialize all the re-used views
} else { // Otherwise re-use the converted view
imageView = (ImageView) convertView;
}
I think the best approach here would be to use an Adapter that returns different types of views (by overriding getViewTypeCount() and getItemViewType()), for example as described in this answer.
That way you do not need to programmatically alter the returned views, at all. Just define two XML layouts and inflate/reuse either one or the other according to whether the item at that position has a video or not.
Not only would this be clearer, you wouldn't have the performance penalty of "transforming" one view into the other whenever a video-row is supplied as convertView for another item without one, or vice-versa
I would make your getView() always return a RelativeLayout object (which I call containerView below) to which you add your ImageView(s) as children.
The only complication here is that you need to give these children identifiers so that you can retrieve them from a recycled convertView later. Note that I used the built-in, static View.generateViewId() for this, which is API level 17. If you need it to work pre-API-level-17 you can create your own ids using unique integers (such as 1, 2, etc.) -- just make sure they aren't greater than 0x0FFFFFF. Update: I added code that I use for this below.
See the comments I added in several points below.
#Override
public View getView(int position, View convertView, ViewGroup container) {
// First check if this is the top row
if (position < mNumColumns) {
if (convertView == null) {
convertView = new View(mContext);
}
// Set empty view with height of ActionBar
//convertView.setLayoutParams(new AbsListView.LayoutParams(
// LayoutParams.MATCH_PARENT, mActionBarHeight));
return convertView;
}
// Now handle the main ImageView thumbnails
RelativeLayout containerView;
ImageView imageView;
ImageView videoIconView; // TODO: or whatever type you want to use for this...
if (convertView == null) { // if it's not recycled, instantiate and initialize
containerView = new RelativeLayout(mContext);
// TODO: The layout params that you used for the image view you probably
// now want to use for the container view instead...
imageView.setLayoutParams(mImageViewLayoutParams); // If so, you can change their name...
imageView = new RecyclingImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
//imageView.setLayoutParams(mImageViewLayoutParams); // This probably isn't needed any more.
// Generate an Id to use for later retrieval of the imageView...
// This assumes it was initialized to -1 in the constructor to mark it being unset.
// Note, this could be done elsewhere in this adapter class (such as in
// the constructor when mImageId is initialized, since it only
// needs to be done once (not once per view) -- I'm just doing it here
// to avoid having to show any other functions.
if (mImageId == -1) {
mImageId = View.generateViewId();
}
imageView.setId(mImageId);
containerView.addView(imageView, RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
// NOTE: At this point, I would personally always just add the video icon
// as a child of containerView here no matter what (generating another unique
// view Id for it, mVideoIconId, similar to how was shown above for the imageView)
// and then set it to either VISIBLE or INVISIBLE/GONE below depending on whether
// the URL contains the word "video" or not.
// For example:
vidoIconView = new <whatever>;
// TODO: setup videoIconView with the proper drawable, scaling, etc. here...
if (mVideoIconId == -1) {
mVideoIconId = View.generateViewId();
}
videoIconView.setId(mVideoIconId);
containerView.addView(videoIconView, RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
final RelativeLayout.LayoutParams layout = ((RelativeLayout.LayoutParams)containerView.getLayoutParams());
layout.addRule(RelativeLayout.LayoutParams.CENTER_HORIZONTAL); // ... or whatever else you want
layout.addRule(RelativeLayout.LayoutParams.ALIGN_PARENT_BOTTOM); // ... or whatever else you want
} else {
// Otherwise re-use the converted view
containerView = (RelativeLayout) convertView;
imageView = containerView.findViewById(mImageId);
videoIconView = containerView.findViewById(mVideoIconId); // see comment above
}
// Check the height matches our calculated column width
if (containerView.getLayoutParams().height != mItemHeight) {
containerView.setLayoutParams(mImageViewLayoutParams);
}
if(images.get(position - mNumColumns).getUriString().contains("video")){
//display video icon
// see comment above, here you can probably just do something like:
videoIconView.setVisibility(View.VISIBLE);
}
else
{
//don't display video icon
videoIconView.setVisibility(View.GONE); // could also use INVISIBLE here... up to you.
}
// Finally load the image asynchronously into the ImageView, this also takes care of
// setting a placeholder image while the background thread runs
if (images != null && !images.isEmpty())
mImageFetcher.loadImage(images.get(position - mNumColumns).getUriString()/*.imageUrls[position - mNumColumns]*/, imageView);
return containerView;
}
Update:
In response the question in the comments, I use something like this (in my custom "ViewController" base class):
private static int s_nextGeneratedId = 1;
/**
* Try to do the same thing as View.generateViewId() when using API level < 17.
* #return Unique integer that can be used with setId() on a View.
*/
protected static int generateViewId() {
// AAPT-generated IDs have the high byte nonzero; clamp to the range under that.
if (++s_nextGeneratedId > 0x00FFFFFF)
s_nextGeneratedId = 1; // Roll over to 1, not 0.
return s_nextGeneratedId;
}
Note that you do not need a unique view id for every single cell in your grid. Rather, you just need it for each type of child view that you might want to access using findViewById(). So in your case, you're probably going to need just two unique ids. Since the view ids auto-generated from your xml layout files into your R.java typically are very large, I've found it convenient just to use low numbers for my hand-generated ids (as shown above).

Setting ImageView width and height before drawing in a ListView adapter

I have an adapter to a ListView is a list of ImageViews. I am using a stretch to make the image fil the imageview so I can take smaller images and make them larger on the screen, however the ImageView normally just uses wrap_content and this is an issue because the images just show up as their normal width and height. Is there any way I can set the height and width of a view before drawing it because as in this case I do not have control over the view after it has been drawn. Here is my aapter method:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
String currentImage = getItem(position);
ScaleType scaleType = ScaleType.FIT_CENTER;
float screenWidth = parent.getResources().getDisplayMetrics().widthPixels;
if (convertView == null) {
convertView = new ImageView(parent.getContext());
}
// WHAT I WOULD LIKE TO BE ABLE TO DO, but this returns null pointer exception
// convertView.getLayoutParams().width = (int) screenWidth;
// convertView.getLayoutParams().height = (int) ((float)(screenWidth/currentImage.getWidth())*currentImage.getHeight());
((ImageView) convertView).setScaleType(scaleType);
((ImageView) convertView).setImageBitmap(MainActivity.cache.getBitmap(currentImage));
return convertView;
}
How about something like someView.setHeight() and someView.setWidth()? Or someView.setLayoutParams()? You could add either of these to the overridden getView() callback and it should take care of your problem.
You could also Create a Custom View and override something like getMeasuredWidthAndState(). (I think that's one of the right methods, but I'm not one hundred percent sure.) You could create a width class variable and a height class variable that all instances of your custom ImageView would use. However, that might be a bit much if you just want to set the layout width and height though.

Gallery with setScaleX/Y on children

I have a gallery feeded by custom adapter using custom views as elements. I need these elements to be scaled by 70% by default. The problem is that gallery behaves like they are at 100% size with 30% transparent padding = spacing between elements are just big. For better understanding I attached two images with elements scaled to 100% and 70%.
Elements scaled to 100%
Elements scaled to 70%
I cannot hardcode the setSpacing as this would behave weird on different resolutions. I tried setSpacing(0) with no luck. How can I achieve that gallery would behave like the elements are small (70%) and not the original size?
I'm scaling the elements by adding setScaleX/Y to the constructor of custom element MagazineCell which extends RelativeLayout:
public MagazineCell(Context context) {
super(context);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
layout = (RelativeLayout) inflater.inflate(R.layout.mag_big, null);
addView(layout);
this.setScaleX(0.7f);
this.setScaleY(0.7f);
}
I also tried to set the scale in drawChild() of the gallery with no luck. In adapter I'm simply using this class for gallery elements:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
MagazineCell cell;
position = getPosition(position);
if (null == convertView || convertView.getClass() != MagazineCell.class) {
cell = new MagazineCell(context);
} else {
cell = (MagazineCell) convertView;
}
return cell;
}
Gallery has no special code. I'm using SDK 11 on Acer Iconia TAB A500 running Android 3.1.
Thanks for any hints or comments.
Looks like there's no way to "trick" Gallery to measure scaled cells with their scaled width. So I just used negative spacing - setSpacing(-100); There may be a way via overriding onMeasure method on MagazineCell class and calculate scaled width. But I didn't try this.
after
this.setScaleX(0.7f);
this.setScaleY(0.7f);
add
this.setPivotX(0);
this.setPivotY(0);

android screen orientation

I tried getOrientation() to get the orientation value but it always returns 0!
getOrientation() is deprecated but this is not neccesarily the root of your problem. It is true that you should use getRotation() instead of getOrientation() but you can only use it if you are targeting Android 2.2 (API Level 8) or higher. Some people and even Googlers sometimes seem to forget that.
As an example, on my HTC desire running Android 2.2. getOrientation() and getRotation() both report the same values:
0 (default, portrait),
1 (device 90 degree counterclockwise),
3 (device 90 degree clockwise)
It does not report when you put it "on its head" (rotate 180, that would be the value 2). This result is possibly device-specific.
First of all you should make clear if you use an emulator or a device. Do you know how to rotate the emulator? Then I would recommend to create a small test program with a onCreate() Method like this:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
WindowManager mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
Display mDisplay = mWindowManager.getDefaultDisplay();
Log.d("ORIENTATION_TEST", "getOrientation(): " + mDisplay.getOrientation());
}
Check if the screen of your your device has been locked in the device settings Settings > Display > Auto-Rotate Screen. If that checkbox is unchecked, Android will not report orientation changes to your Activity. To be clear: it will not restart the activity. In my case I get only 0, like you described.
You can check this from your program if you add these lines to onCreate()
int sysAutoRotate = 0;
try {
sysAutoRotate = Settings.System.getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION);
} catch (SettingNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Log.d("ORIENTATION_TEST", "Auto-rotate Screen from Device Settings:" + sysAutoRotate);
It will return 0 if Auto-Rotate is off and 1 if Auto-Rotate is on.
Another try. In your original program you might have set in manifest
android:screenOrientation="portrait"
Same effect, but this time for your activity only. If you made the small test program this possibilty would have been eliminated (that's why I recommend it).
Remark: Yes the whole orientation / rotation topic is an "interesting" topic indeed. Try things out, use Log.d(), experiment, learn.
If you want to know if the content currently displayed is in landscape mode or portrait (possibly completely independent of the phone's physical rotation) you can use:
getResources().getConfiguration().orientation
Developer Documentation
public int orientation
Since: API Level 1
Overall orientation of the screen. May be one of ORIENTATION_LANDSCAPE, ORIENTATION_PORTRAIT, or ORIENTATION_SQUARE.
getOrientation() is deprecated. Instead, try getRotation().
To avoid use of Deprecated methods use the Android Configuration class found here. It works since API lvl 1 and still works on the latest android devices. (Not deprecated).
As and example consider the following code snippet:
Configuration config = getResources().getConfiguration();
if (config.orientation == Configuration.ORIENTATION_PORTRAIT)
{
setContentView(R.layout.portraitStart);
}
else
{
setContentView(R.layout.landscapeStart);
}
Best of luck- hope this answer helps whoever runs into it.
You should use:
getResources().getConfiguration().orientation
It can be one of ORIENTATION_LANDSCAPE, ORIENTATION_PORTRAIT.
http://developer.android.com/reference/android/content/res/Configuration.html#orientation
/* First, get the Display from the WindowManager */
Display display = ((WindowManager) getSystemService(WINDOW_SERVICE)).getDefaultDisplay();
/* Now we can retrieve all display-related infos */
int width = display.getWidth();
int height = display.getHeight();
int orientation = display.getOrientation();
I think you need to create two different layouts and inflate different layouts on different orientation. I am giving you a sample code which is working fine for me.
"context" you can pass from your activity in case of custom adapter.
If you are using custom adapter then you can try this code:
#Override
public View getView(final int position,
View convertView,
ViewGroup parent)
{
View rowView = convertView;
ViewHolder holder = null;
int i = context.getResources().getConfiguration().orientation;
if (rowView == null) {
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (i == Configuration.ORIENTATION_PORTRAIT) {
rowView = inflater.inflate(R.layout.protrait_layout, null, false);
} else {
rowView = inflater.inflate(R.layout.landscape_layout, null, false);
}
holder = new ViewHolder();
holder.button = (Button) rowView.findViewById(R.id.button1);
rowView.setTag(holder);
} else {
holder = (ViewHolder) rowView.getTag();
}
return rowView;
}
other wise you can directly set two different layouts in your code
int i = context.getResources().getConfiguration().orientation;
if (i == Configuration.ORIENTATION_PORTRAIT) {
ArrayAdapter<String> adt = new ArrayAdapter<String>(getApplicationContext(), R.layout.protrait_layout, name);
} else {
ArrayAdapter<String> adt = new ArrayAdapter<String>(getApplicationContext(), R.layout.landscape_layout, name);
}
I had the same problem on NexusOne SDK 2.3.
This solved it: Check orientation on Android phone.

Categories

Resources