What I am trying to create is a horizontal scrolling image gallery. I have a RecyclerView (support 22.0.0). The problem I am having is that when I scroll to the end and then scroll back, usually one image will be missing sometimes two. Strangely when I keep swiping back and forth, a different image could be missing. Here is the layout for the item:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="160dp">
<ImageView
android:id="#+id/product_variation_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:layout_gravity="center"/>
Here is the Adaper:
public class TestAdapter extends RecyclerView.Adapter<TestAdapter.ViewHolder> {
private String[] mDataset;
public static class ViewHolder extends RecyclerView.ViewHolder {
public ImageView mImageView;
public ViewHolder(View v) {
super(v);
mImageView = (ImageView) v.findViewById(R.id.product_variation_image);
}
}
public TestAdapter(String[] myDataset) {
mDataset = myDataset;
}
#Override
public TestAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
// create a new view
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.variaton_list_item, parent, false);
ViewHolder vh = new ViewHolder(v);
return vh;
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.mImageView.setImageDrawable(null);
String url = mDataset[position];
Log.i("TEST", "position = " + position);
((MainActivity)MainActivity.getInstance()).imageDownloader.download(url, holder.mImageView);
}
#Override
public int getItemCount() {
return mDataset.length;
}
The download method fetches the image, from a URL or gets it from the memory if it has been cached. This works fine in all other layouts e.g. ListView or GridView. Here is the code I use to set it up in the Fragment:
final LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
mRecyclerView.setLayoutManager(layoutManager);
This is in the onCreateView method. When I get the urls I populate them and set the adapter using:
myDataset[i] = imageURL; // for each image
mAdapter = new TestAdapter(myDataset);
mRecyclerView.setAdapter(mAdapter);
The interesting thing is the line in the onBindViewHolder method in the adapter, where I log the position. What I have found is that cells where the image is not shown is that this method is not being called. It is like it is skipping that cell for some reason. Even stranger, if I hold a cell and keep swiping from left to right, if a cell goes off screen and then comes back in, its image as gone as again the onBindViewHolder method is not called.
Would it be possible to test something out? Could you use this library to load the images from the URLs ? http://square.github.io/picasso/ It caches everything and it handles everything in an async manner.
Use it something like ...
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Picasso.with(mImageView.getContext()).cancelRequest(holder.mImageView);
String url = mDataset[position];
Picasso.with(mImageView.getContext()).load(url).placeholder(R.drawable.placeholder).into(holder.mImageView);
}
... and see if it still doesn't display some images. If it does, then at least you'll be 100% sure the problem is not in your downloading mechanism (which I think it might be).
If you're using Android Studio then just add the dependency compile 'com.squareup.picasso:picasso:2.5.2', if not you can add the library you find at the above link.
It's worth a try ...
The one class that I did not think would matter was the one that was causing the issue. I am not sure what the reason is, but it resides in a custom ImageView class that I am using for recycling that I got from the BitmapFun sample.
public class RecyclingImageView extends ImageView {
public RecyclingImageView(Context context) {
super(context);
}
public RecyclingImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* #see android.widget.ImageView#onAttachedToWindow()
*/
#Override
protected void onAttachedToWindow() {}
/**
* #see android.widget.ImageView#onDetachedFromWindow()
*/
#Override
protected void onDetachedFromWindow() {
// This has been detached from Window, so clear the drawable
setImageDrawable(null);
super.onDetachedFromWindow();
}
/**
* #see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)
*/
#Override
public void setImageDrawable(Drawable drawable) {
// Keep hold of previous Drawable
final Drawable previousDrawable = getDrawable();
// Call super to set new Drawable
super.setImageDrawable(drawable);
// Notify new Drawable that it is being displayed
notifyDrawable(drawable, true);
// Notify old Drawable so it is no longer being displayed
notifyDrawable(previousDrawable, false);
}
/**
* Notifies the drawable that it's displayed state has changed.
*
* #param drawable
* #param isDisplayed
*/
private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) {
if (drawable instanceof RecyclingBitmapDrawable) {
// The drawable is a CountingBitmapDrawable, so notify it
((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed);
} else if (drawable instanceof LayerDrawable) {
// The drawable is a LayerDrawable, so recurse on each layer
LayerDrawable layerDrawable = (LayerDrawable) drawable;
for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {
notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);
}
}
}
}
When I replace this with a normal ImageView, I no longer get the problem.
We can fix the issue by extends LinearLayoutManager and ImageView.
1. Creats a PrecachingLinearLayoutManager
public class PrecachingLinearLayoutManager extends LinearLayoutManager {
private static final int DEFAULT_EXTRA_LAYOUT_SPACE = 600;
private int extraLayoutSpace = -1;
#SuppressWarnings("unused")
private Context mContext;
public PrecachingLinearLayoutManager(Context context) {
super(context);
this.mContext = context;
}
public PrecachingLinearLayoutManager(Context context, int extraLayoutSpace) {
super(context);
this.mContext = context;
this.extraLayoutSpace = extraLayoutSpace;
}
public PrecachingLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
this.mContext = context;
}
public void setExtraLayoutSpace(int extraLayoutSpace) {
this.extraLayoutSpace = extraLayoutSpace;
}
#Override
protected int getExtraLayoutSpace(RecyclerView.State state) {
if (extraLayoutSpace > 0) {
return (extraLayoutSpace);
}
return (DEFAULT_EXTRA_LAYOUT_SPACE);
}
}
2. Use PrecachingLinearLayoutManager to replace LinearLayoutManager
DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
PrecachingLinearLayoutManager layout = new PrecachingLinearLayoutManager(getActivity());
layout.setExtraLayoutSpace(displayMetrics.heightPixels);
recyclerview.setLayoutManager(layout);
3. Creats a RecycleImageView
private Object tag = null;
#Override
protected void onAttachedToWindow() {
Object tag = getTag();
if (tag == null || !tag.equals(this.tag)) {
// Will cause displayed bitmap wrapper to
// be 'free-able'
setImageDrawable(null);
this.tag = null;
super.onDetachedFromWindow();
}
super.onAttachedToWindow();
}
#Override
protected void onDetachedFromWindow() {
Object tag = getTag();
if (tag != null) {
this.tag = tag;
} else {
// Will cause displayed bitmap wrapper to
// be 'free-able'
setImageDrawable(null);
this.tag = null;
super.onDetachedFromWindow();
}
}
4. Use RecycleImageView to replace ImageView
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:extends="http://schemas.android.com/apk/res/com.yourdomain.yourpackage"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/viewgroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<com.yourdomain.yourpackage.RecycleImageView
android:id="#+id/photo"
android:layout_width="40dp"
android:layout_height="40dp"
extends:delayable="true"
android:contentDescription="#string/nothing"
android:src="#drawable/photo_placeholder" >
</com.yourdomain.yourpackage.RecycleImageView>
</LinearLayout>
Related
I am having Recycler View. It's like a grid view. A total of 9 images in grid layout. If I click a image in any one of the above, that image have to change to an another image. If I click another image. Last one want to reset. Then the clicked image alone will change to highlighted image.
Here is my code...
holder.mLayout.setOnClickListener(new View.OnClickListener() {
#Override public void onClick(View view) {
//for (int i = 0; i < data_collection.size(); i++) {
holder.mLayout.setVisibility(View.VISIBLE);
holder.mHighLighted.setVisibility(View.GONE);
if (position == i) {
}
//}
holder.mLayout.setVisibility(View.GONE);
holder.mHighLighted.setVisibility(View.VISIBLE);
mHighLight.onHighLight(position, view);
}
});
Remove what you dont need.
#Override
public void onBindViewHolder(final SimpleViewHolder holder, final int position) {
holder.textView.setText(elements.get(position).getName());
holder.textView.setTypeface(typeface1);
CircularImageView circularImageView = (CircularImageView) holder.linearLayout.findViewById(R.id.personazhe_layout_grid_item_image);
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// circularImageView.setBackground(elements.get(position).getPhoto());
// }
circularImageView.setImageDrawable(elements.get(position).getProfileImage());
//Picasso.with(context).load(elements.get(position).getProfileImage()).into(circularImageView);
holder.linearLayout.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if(pos != position){
c.setImageDrawable(elements.get(position).getProfileImage());
t.setText(elements.get(position).getName());
seekBar.setProgress(0);
pos = position;
}
//image = elements.get(position).getProfileImage();
// textviews
// trajneri = elements.get(position).getTrajneri();
// mosha = elements.get(position).getMosha();
// vendbanimi = elements.get(position).getVendbanimi();
// vendlindja = elements.get(position).getVendlindja();
// arsimi = elements.get(position).getArsimi();
// name = elements.get(position).getName();
// surname = elements.get(position).getSurname();
// pos = elements.get(position).number();
// posi = position;
// button.performClick();
}
});
}
The ViewHolder pattern is something that Android pushed developers to use for a long time, and then (rightfully) forced on them with RecyclerViews. The idea, opposed to a simple ListView, is that you reuse as much of the view as possible when scrolling to reduce inflation and resource identification. The ViewHolder should be managed as something that is changed/not created within the RecyclerView.
Because of that, storing information in a ViewHolder that must be persistent will not work. For that, there are a plethora of other options. Let's go with an inner class that will manage holding onto the currently selected view position and its relative images.
Let's say we have a custom ViewHolder like below:
public class ImageViewHolder extends RecyclerView.ViewHolder{
private ImageView iv;
public ImageViewHolder(View v){
iv = (ImageView) v.findViewById(R.id.iv);
}
public ImageVie getImageView(){
return iv;
}
}
And utilizing that view holder is an adapter DemoAdapter, we can modify it to look something like this:
public class DemoAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
public interface SelectionListener{
void onImageSelected(Bitmap bmp);
}
private static class SelectionHolder{
protected int position;
protected Bitmap originalBmp, newBmp;
public SelectionHolder(int position, Bitmap originalBmp,
Bitmap newBmp){
this.position = position;
this.originalBmp = originalBmp;
this.newBmp = newBmp
}
}
private SelectionHolder selectionHolder;
private SelectionListener selectionListener;
/*
Pre-existing Adapter functionality
*/
public void setSelectionListener(SelectionListener listener){
selectionListener = listener;
}
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
/*
Pre-existing onBindViewHolder code
*/
ImageView iv = holder.getImageView();
if(selectionHolder != null && selectionHolder.position == position)
iv.setImageBitmap(selectionHolder.newBmp);
else{
//set the image however you are doing it now
}
iv.setOnClickListener(
new new View.OnClickListener() {
#Override
public void onClick(View v) {
ImageView iv = (ImageView) v;
// Get the IV's current bmp
Bitmap originalBmp = getBitmapFromImageView(iv);
// Get the currently selected image's "new" image
// if it is null, set it to the original bmp
// this will initialize our "highlighting"
Bitmap newBmp = selectionHolder == null || selectionHolder.newBmp == null?
originalBmp: selectoinHolder.newBmp;
// set the selection holder
selectionHolder = new SelectionHolder(position, originalBmp, newBmp);
// notify our listener
if(selectionListener != null)
selectionListener.onImageSelected(bmp);
// refresh the adapter
DemoAdapter.this.notifyDataSetChanged();
}
});
}
private Bitmap getBitmapFromImageView(ImageView iv){
return ((BitmapDrawable)(iv.getDrawable()).getBitmap()
}
}
Then if we have an activity that needs the selected image, perhaps to display it in an ImageView it hosts
recyclerAdapter = new DemoAdapter(...);
recyclerAdapter.setSelectionListener(new SelectionListener(){
#Override
public void onImageSelected(Bitmap bmp){
// set the bmp to your image view or whatever you want
}
}
I have been working on this issue for days now. I am using the kankan Android wheel example/library, but am wanting to dynamically add images to the wheel when a button is pressed. The image added depends on the button's text. It seems like a fairly easy task, but perhaps I am missing something. I tried calling the adapter's notifyDataChangedEvent() after passing and adding the selected image to the adapter's list of cached images. Debugging has showed that the images were being added to the list of images, but they are not showing up on the wheel. If someone could please help me out with this problem I would appreciate it!
Code:
public void addItem(String text) {
for(Item c: Item.values()){
if(c.getName().equals(text)) {
slotMachineAdapter.addImage(c.getImage());
break;
}
}
slotMachineAdapter.notifyDataChangedEvent();
}
Adapter
private class SlotMachineAdapter extends AbstractWheelAdapter {
// Image size
final int IMAGE_WIDTH = 700;
final int IMAGE_HEIGHT = 150;
// Slot machine symbols
private final int items[] = new int[] {
R.mipmap.ic_flipper
};
// Cached images
private List<SoftReference<Bitmap>> images;
// Layout inflater
private Context context;
/**
* Constructor
*/
public SlotMachineAdapter(Context context) {
this.context = context;
images = new ArrayList<SoftReference<Bitmap>>();
for (int id : items) {
images.add(new SoftReference<Bitmap>(loadImage(id)));
}
}
/**
* Loads image from resources
*/
private Bitmap loadImage(int id) {
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), id);
Bitmap scaled = Bitmap.createScaledBitmap(bitmap, IMAGE_WIDTH, IMAGE_HEIGHT, true);
bitmap.recycle();
return scaled;
}
#Override
public int getItemsCount() {
return items.length;
}
// Layout params for image view
final ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(IMAGE_WIDTH, IMAGE_HEIGHT);
#Override
public View getItem(int index, View cachedView, ViewGroup parent) {
ImageView img;
if (cachedView != null) {
img = (ImageView) cachedView;
} else {
img = new ImageView(context);
}
img.setLayoutParams(params);
SoftReference<Bitmap> bitmapRef = images.get(index);
Bitmap bitmap = bitmapRef.get();
if (bitmap == null) {
bitmap = loadImage(items[index]);
images.set(index, new SoftReference<Bitmap>(bitmap));
}
img.setImageBitmap(bitmap);
return img;
}
//Adds image to list of images
public void addImage(int img){
images.add(new SoftReference<Bitmap>(loadImage(img)));
}
}
Because the count you return referenced to items variable, But addImage function did not change items size. Try to change your code like below and test it again:
#Override
public int getItemsCount() {
return images.size();
}
I've implemented a RecyclerView with CardView. Each CardView has an ImageView which I want to change its background color depending on the result of a Query (Empty result set -> Grey / Non Empty result set -> Red), this is implemented on the onBindViewHolder of the RecyclerView Adapter.
Here's the Adapter's Code (I've removed most of the code for the sake of clarity):
public class FavDirsAdapter extends RecyclerView.Adapter<FavDirsAdapter.FavDirsViewHolder> {
private LayoutInflater mInflater;
private Cursor mCursor;
private Context mContext;
private int range;
private FragmentManager mFragmentManager;
public FavDirsAdapter(Context context, Cursor cursor, FragmentManager fm) {
mInflater = LayoutInflater.from(context);
mCursor = cursor;
mContext = context;
range = cursor.getCount();
mFragmentManager = fm;
}
#Override
public FavDirsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View view = mInflater.inflate(R.layout.item_fav_dirs_list, parent, false);
return new FavDirsViewHolder(view);
}
#Override
public void onBindViewHolder(final FavDirsViewHolder viewHolder, final int position) {
if (mCursor.moveToFirst()) {
mCursor.moveToPosition(position);
viewHolder.favDirsItemTextView.setText(mCursor.getString(mCursor.getColumnIndex(
(FilmoContract.FavDirEntry.COLUMN_DIR))));
getDirImage(viewHolder);
setScheduledFilmsColor(viewHolder);
}
}
#Override
public int getItemCount() {
if (mCursor.moveToFirst()) {
return range;
}
return 0;
}
class FavDirsViewHolder extends RecyclerView.ViewHolder{
TextView favDirsItemTextView;
ImageView favDirsItemImageView;
ImageView favDirsItemScheduledFilmsImage;
public FavDirsViewHolder(View itemView) {
super(itemView);
favDirsItemTextView = (TextView) itemView.findViewById(R.id.fav_dirs_item_text_view);
favDirsItemImageView = (ImageView) itemView.findViewById(R.id.fav_dirs_item_image_view);
favDirsItemScheduledFilmsImage =
(ImageView) itemView.findViewById(R.id.fav_dirs_item_scheduled_films_image_button);
}
}
private void setScheduledFilmsColor(FavDirsViewHolder viewHolder) {
new SetScheduledFilmsColor(viewHolder, mContext).execute();
}
private void getDirImage (FavDirsViewHolder viewHolder) {
new FetchFavDirsImage(viewHolder, mContext).execute();
}
}
The adapter receive a Cursor with a Films Director data list stored on a DB. Then assign each row data to a ViewHolder fileds.
Then I make an additional query to determine wheter each Director has movies scheduled soon, and depending on the query result, color the ImageView (favDirsItemScheduledFilmsImage) background.
I tried first to make the query on the onBindViewHolder Adapter's method, but I've found that all the ImageViews, where colored in the same color (Grey). So since there is a DB Query involved, I've tried to create an AsyncTask (SetScheduledFilmsColor) to do all that stuff. Here's the code:
public class SetScheduledFilmsColor extends AsyncTask<Void, Void, Boolean> {
private ImageView mImageButton;
private String mDirName;
private Context mContext;
boolean scheduledFilms;
static final String[] PROGRAM_COLUMNS = {
FilmoContract.FilmEntry._ID,
FilmoContract.FilmEntry.COLUMN_DATE,
FilmoContract.FilmEntry.COLUMN_TIME,
FilmoContract.FilmEntry.COLUMN_CYCLE,
FilmoContract.FilmEntry.COLUMN_TITLE,
};
public SetScheduledFilmsColor(FavDirsAdapter.FavDirsViewHolder viewHolder, Context context) {
mImageButton = viewHolder.favDirsItemScheduledFilmsImage;
//mImageButton = new ImageButton(mContext);
mDirName = viewHolder.favDirsItemTextView.getText().toString();
mContext = context;
}
#Override
protected Boolean doInBackground(Void... params) {
Uri filmoDirector = FilmoContract.FilmEntry.buildProgramUriWithDirector();
Cursor tempCursor = mContext.getContentResolver().query(
filmoDirector,
PROGRAM_COLUMNS,
mDirName,
null,
null
);
scheduledFilms = tempCursor.moveToFirst();
tempCursor.close();
return scheduledFilms;
}
#Override
protected void onPostExecute(Boolean scheduledFilms) {
super.onPostExecute(scheduledFilms);
mImageButton.setImageResource(R.drawable.ic_new_releases_white_24dp);
if (scheduledFilms) {
//mImageButton.getBackground().clearColorFilter();
mImageButton.getBackground().setColorFilter(
mContext.getResources().getColor(R.color.lafilmo_color), PorterDuff.Mode.MULTIPLY
);
} else {
//mImageButton.getBackground().clearColorFilter();
mImageButton.getBackground().setColorFilter(
mContext.getResources().getColor(R.color.dividers), PorterDuff.Mode.MULTIPLY
);
}
}
}
I'm changing the color in the onPostExecute method. However, even doing this asynchronously, it doesn't matter the query result (I'm sure the query and the onPostExecute condition are fine, I have debug that). Even more, each time I reload the RecyclerView Fragment while navigating through my App, the colored ImageViews are different each time.
I don't understand, how RecyclerView can maintain the reference correctly to the other CardView fields (the fileds on the viewHolder, like favDirsItemTextView or favDirsItemImageView) which are assigned on the onBindViewHolder, and not maintain a reference to the colored ImageView (favDirsItemScheduledFilmsImage).
Can anybody shed some light on this? Is there a better way to do this?
Thanks!
i have a simple question:
suppose i have some views on a scrollView (or a horizontalScrollView) .
is there any way to add a listener that will tell me when such a view is getting inside and outside the visible area ?
the only similar question i've seen is this:
Android: how to check if a View inside of ScrollView is visible?
but i want to be informed when such an event occurs (becoming hidden/visible) .
Subclass the view classes you are using (I did this for ImageView as I was only adding those to my scroll view):
public class PeekImageView extends ImageView implements ViewTreeObserver.OnScrollChangedListener {
private static final String LOG_TAG = "PeekImageView";
private InViewportListener inViewportListener;
private boolean isInViewport = false;
public PeekImageView(Context context) {
super(context);
}
public PeekImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PeekImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public interface InViewportListener {
void onViewportEnter(PeekImageView view);
void onViewportExit(PeekImageView view);
}
public void setInViewportListener(InViewportListener listener) {
this.inViewportListener = listener;
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
ViewTreeObserver vto = getViewTreeObserver();
if (vto != null) {
vto.addOnScrollChangedListener(this);
}
}
#Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
ViewTreeObserver vto = getViewTreeObserver();
if (vto != null) {
vto.removeOnScrollChangedListener(this);
}
}
#Override
public void onScrollChanged() {
Rect bounds = new Rect();
boolean inViewport = getLocalVisibleRect(bounds);
Log.d(LOG_TAG, "is in view " + bounds + " : " + inViewport + " ; " + bounds);
if (inViewportListener != null && isInViewport != inViewport) {
if (inViewport) {
inViewportListener.onViewportEnter(this);
} else {
inViewportListener.onViewportExit(this);
}
}
isInViewport = inViewport;
}
}
Attaching an InViewportListener to an instance of this PeekImageView will get you notified whenever the view enters or leaves the visible part of the window (the viewport).
You could do something like:
1) keep a list/array of views that are contained in your ScrollView.
2) Set a listener on the scroll view for when the scroll is changed: Synchronise ScrollView scroll positions - android
3) In the listener loop through these views using the Android: how to check if a View inside of ScrollView is visible? method to see if they have gone of the screen
This is a basic method but it'll work, how fast it is depends on whats on your screen etc, but it starts you in the right direction
I've found a nice way to be notified of what i've asked about here.
it works for scrollView with vertical LinearLayout, but if you wish you can make it work for other cases too, depending on the case.
i'm not sure if i should handle onSizeChanged() method too, and if so, what to do there, but in all other cases, this code works fine.
here's the code:
MainActivity.java (for testing) :
public class MainActivity extends Activity
{
#Override
protected void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final CustomScrollView scrollView=(CustomScrollView)findViewById(R.id.scrollView1);
scrollView.setOnChildViewVisibilityChangedListener(new onChildViewVisibilityChangedListener()
{
#Override
public void onChildViewVisibilityChanged(final int index,final View v,final boolean becameVisible)
{
Log.d("Applog","index:"+index+" visible:"+becameVisible);
}
});
final ViewGroup container=(ViewGroup)findViewById(R.id.linearLayout);
for(int i=0;i<20;++i)
{
final TextView tv=new TextView(this);
tv.setText("item "+i);
tv.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,300));
container.addView(tv);
}
}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.scrollviewvisibilitydetector.CustomScrollView
android:id="#+id/scrollView1"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<LinearLayout android:id="#+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
</LinearLayout>
</com.example.scrollviewvisibilitydetector.CustomScrollView>
</RelativeLayout>
CustomScrollView.java (the real deal...) :
public class CustomScrollView extends ScrollView
{
Set<Integer> _shownViewsIndices =new HashSet<Integer>();
onChildViewVisibilityChangedListener _onChildViewVisibilityChangedListener;
public interface onChildViewVisibilityChangedListener
{
public void onChildViewVisibilityChanged(int index,View v,boolean becameVisible);
}
public CustomScrollView(final Context context)
{
super(context);
}
public CustomScrollView(final Context context,final AttributeSet attrs)
{
super(context,attrs);
}
public CustomScrollView(final Context context,final AttributeSet attrs,final int defStyle)
{
super(context,attrs,defStyle);
}
public void setOnChildViewVisibilityChangedListener(final onChildViewVisibilityChangedListener onChildViewVisibilityChangedListener)
{
_onChildViewVisibilityChangedListener=onChildViewVisibilityChangedListener;
}
#Override
protected void onLayout(final boolean changed,final int l,final int t,final int r,final int b)
{
super.onLayout(changed,l,t,r,b);
checkViewsVisibility(l,t);
}
private void checkViewsVisibility(final int l,final int t)
{
final ViewGroup viewGroup=(ViewGroup)getChildAt(0);
final int childCount=viewGroup.getChildCount();
if(childCount==0)
return;
final int parentBottom=t+getHeight();
// prepare to use binary search to find a view that is inside the bounds
int min=0,max=childCount-1,piv=-1;
int childTop,childBottom;
View v;
// check previously shown views
for(final Iterator<Integer> iterator=_shownViewsIndices.iterator();iterator.hasNext();)
{
final Integer cur=iterator.next();
v=viewGroup.getChildAt(cur);
childTop=v.getTop();
childBottom=v.getBottom();
if(childTop<=parentBottom&&childBottom>=t)
{
if(piv==-1)
piv=cur;
}
else
{
if(_onChildViewVisibilityChangedListener!=null)
_onChildViewVisibilityChangedListener.onChildViewVisibilityChanged(cur,v,false);
iterator.remove();
}
}
if(piv==-1)
{
// check first view
v=viewGroup.getChildAt(min);
childTop=v.getTop();
childBottom=v.getBottom();
if(childTop<=parentBottom&&childBottom>=t)
piv=min;
else
{
// check last view
v=viewGroup.getChildAt(max);
childTop=v.getTop();
childBottom=v.getBottom();
if(childTop<=parentBottom&&childBottom>=t)
piv=min;
}
if(piv==-1)
while(true)
{
piv=(min+max)/2;
v=viewGroup.getChildAt(piv);
childTop=v.getTop();
childBottom=v.getBottom();
if(childTop<=parentBottom&&childBottom>=t)
break;
if(max-min==1)
return;
if(childBottom<t)
// view above bounds
min=piv;
else max=piv;
}
}
//
for(int i=piv;i<childCount;++i)
{
v=viewGroup.getChildAt(i);
childTop=v.getTop();
childBottom=v.getBottom();
// _shownViewsIndices.
if(childTop<=parentBottom&&childBottom>=t&&!_shownViewsIndices.contains(i))
{
_shownViewsIndices.add(i);
if(_onChildViewVisibilityChangedListener!=null)
_onChildViewVisibilityChangedListener.onChildViewVisibilityChanged(i,v,true);
}
}
for(int i=piv-1;i>=0;--i)
{
v=viewGroup.getChildAt(i);
childTop=v.getTop();
childBottom=v.getBottom();
if(childTop<=parentBottom&&childBottom>=t&&!_shownViewsIndices.contains(i))
{
_shownViewsIndices.add(i);
if(_onChildViewVisibilityChangedListener!=null)
_onChildViewVisibilityChangedListener.onChildViewVisibilityChanged(i,v,true);
}
}
}
#Override
protected void onScrollChanged(final int l,final int t,final int oldl,final int oldt)
{
super.onScrollChanged(l,t,oldl,oldt);
checkViewsVisibility(l,t);
}
}
I have a custom configuration page in my app which just so happens to contain a ListView which you can select/deselect, edit, add to and remove items from. Since the amount of configuration is so large I've had to put it all in a ScrollView
My problem is of course that you cannot have scroll functionality within a view which already has it's own scroll functionality. This means I can't have a scrolling ListView inside a ScrollView.
What I've been trying to do is find the best way of limiting the damage this does. I've seen suggestions that say "You could just create a LinearLayout which grows as you add more children". That would work find by the added effort required to plug in the selectable nature, the reordering & sorting of the list as well as the editing would be a maintanance nightmare.
I've spent the day trying to find a way of measuring the height of each ListView item. Once I can find the size of each item (not just the content but any padding and space between items) on each device I know I can simply change the height of the ListView per item added.
Unfortunately I can't seem to find a way to reliably pull back the height of a listviews child.
(The old chestnut of using a GlobalLayoutListener doesn't help me pull back the padding between items)
final TextView listLabel = (TextView) toReturn.findViewById(R.id.listLabel);
final ViewTreeObserver vto = listLabel.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
listLabel.getViewTreeObserver().removeGlobalOnLayoutListener(this);
mListItemHeight = listLabel.getHeight();
}
});
Maybe you're trying to display too many details on your page? You could split the activity in a summary with buttons that lead to multiple one-screen-long activities.
In my experience, users usually prefer an uncluttered and clear view, even if that means having to click once or twice to get to the part they want.
EDIT
Expanding ListView's are you're friend - This LinearLayout expands based on it's content. It allows Dynamic ListView's inside of ScrollView.
public class LinearListView extends LinearLayout {
private BaseAdapter mAdapter;
private Observer mObserver;
private OnItemClickListener mOnItemClickListener;
private OnItemLongClickListener mOnItemLongClickListener;
public LinearListView(Context context) {
super(context);
init();
}
public LinearListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public LinearListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mObserver = new Observer();
}
public void setAdapter(BaseAdapter adapter) {
if (this.mAdapter != null)
this.mAdapter.unregisterDataSetObserver(mObserver);
this.mAdapter = adapter;
adapter.registerDataSetObserver(mObserver);
mObserver.onChanged();
}
public void setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
mOnItemLongClickListener = listener;
}
private int mListSelector = R.drawable.selector_list;
public void setListSelector(int resid) {
mListSelector = resid;
}
private class Observer extends DataSetObserver {
public Observer(){}
#Override
public void onChanged() {
List<View> oldViews = new ArrayList<View>(getChildCount());
for (int i = 0; i < getChildCount(); i++)
oldViews.add(getChildAt(i));
Iterator<View> iter = oldViews.iterator();
removeAllViews();
for (int i = 0; i < mAdapter.getCount(); i++) {
final int index = i;
View convertView = iter.hasNext() ? iter.next() : null;
View toAdd = mAdapter.getView(i, convertView, LinearListView.this);
toAdd.setBackgroundResource(mListSelector);
toAdd.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
if(mOnItemClickListener != null) {
mOnItemClickListener.onItemClick(null, v, index, index);
}
}
});
toAdd.setOnLongClickListener(new OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
if(mOnItemLongClickListener != null) {
mOnItemLongClickListener.onItemLongClick(null, v, index, index);
}
return true;
}
});
LinearListView.this.addView(toAdd, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
}
super.onChanged();
}
#Override
public void onInvalidated() {
removeAllViews();
super.onInvalidated();
}
}
}