androidlistviewconvertviewandroid-viewholder

ViewHolder messing Views


I have implemented both a ViewHolder and a convertView in my listView. My listView is populated by a custom adapter, with a list of bookings. When I click on an item, an invisible layout slides in from right to left, to display buttons. I can dismiss this overlaying layout by clicking on a dismiss button so that it gets hidden again. On this overlaying layout, I have a delete Button, which enables me to delete the item. So far so good. When I erase an item the item disappears as expected, the adapter is then reloaded. The item below takes the position of the deleted item, but remains invisible. I know it is here, because I can still click on the item to trigger the overlaying View. So the ovelaying view is visible but not the item. I have no idea why this is happening. I suspect the ViewHolder to be responsible of this behaviour, but I can't find a solution. Thank you for your help.

See video here : http://youtu.be/KBGEvbUq-V0

My Bookings Class :

public class BookingsListFragment extends Fragment {

private final String SHOP_NAME_KEY = "ShopName";
private final String SHOP_ADDRESS_KEY = "ShopAddress";
public static int mSelectedItem = -1;
private static ListView mBookingsListView;
private static BookingsListViewAdapter mBookingsListViewAdapter;
private static ArrayList<Booking> mBookings;



@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ImageLoader.getInstance().init(ImageLoaderConfiguration.createDefault(getActivity()));
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.bookings_list_fragment, container, false);
    configureListView(view);
    return view;
}

@Override
public void onResume() {
    super.onResume();
    mSelectedItem = -1;
}

private void configureListView(View view) {
    mBookings = BookingsHandler.getBookings();
    mBookingsListView = (ListView) view.findViewById(R.id.bookingsListView);
    mBookingsListViewAdapter = new BookingsListViewAdapter();
    mBookingsListView.setAdapter(mBookingsListViewAdapter);
    mBookingsListView.setTextFilterEnabled(true);
}

public static void updateBookingsListView(ArrayList<Booking> mBookingsList){
    mBookings = mBookingsList;
    mBookingsListViewAdapter.notifyDataSetChanged();
}


static class ViewHolder {
    LinearLayout bookingItemLL;
    RelativeLayout optionsOverlay;
    TextView productName;
    TextView price;
    TextView shopName;
    TextView endDate;
    ImageView productImage;
    LinearLayout placeholderLL;
    Button cancelBooking;
    Button displayDirections;
    Button callShop;
    ImageView discardOverlay;
}


private class BookingsListViewAdapter extends BaseAdapter {

    private static final int TYPE_ITEM = 0;
    private static final int TYPE_PLACEHOLDER = 1;

    @Override
    public int getCount() {
        if (mBookings != null)
            return mBookings.size();
        else
            return 1;
    }

    @Override
    public Object getItem(int position) {
        return position;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public int getItemViewType(int position) {
        // Define a way to determine which layout to use
        if (mBookings != null && mBookings.size() > 0)
            return TYPE_ITEM;
        else
            return TYPE_PLACEHOLDER;
    }

    @Override
    public int getViewTypeCount() {
        return 2; // Number of different layouts
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup viewGroup) {

        int type = getItemViewType(position);

        final ViewHolder holder;

        if(convertView == null) {
            holder = new ViewHolder();

            switch (type){
                case TYPE_ITEM :
                    convertView = LayoutInflater.from(getActivity()).inflate(R.layout.bookings_item,     null);

                    holder.bookingItemLL = (LinearLayout) convertView.findViewById(R.id.bookingItemLL);
                    holder.optionsOverlay = (RelativeLayout) convertView.findViewById(R.id.bookingOptionsOverlay);
                    holder.productName = (TextView) convertView.findViewById(R.id.bookingProductName);
                    holder.price = (TextView) convertView.findViewById(R.id.bookedProductPrice);
                    holder.shopName = (TextView) convertView.findViewById(R.id.bookingShopName);
                    holder.endDate = (TextView) convertView.findViewById(R.id.bookingEndDate);
                    holder.productImage = (ImageView) convertView.findViewById(R.id.bookedProductImage);
                    holder.displayDirections = (Button) convertView.findViewById(R.id.routeShop);
                    holder.cancelBooking = (Button) convertView.findViewById(R.id.cancelBooking);
                    holder.callShop = (Button) convertView.findViewById(R.id.callShop);
                    holder.discardOverlay = (ImageView) convertView.findViewById(R.id.discardOverlay);

                    break;
                case TYPE_PLACEHOLDER :
                    convertView = LayoutInflater.from(getActivity()).inflate(R.layout.booking_placeholder, null);
                    holder.placeholderLL = (LinearLayout) convertView.findViewById(R.id.placeHolderLL);
                    break;
            }
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder)convertView.getTag();
        }

        if(type == 0) {

            if(position == mSelectedItem){
                holder.optionsOverlay.setVisibility(View.VISIBLE);
                configureOverlayButtons(holder);
            }

            holder.bookingItemLL.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(mSelectedItem != position && mSelectedItem != -1){
                        View item = mBookingsListView.getChildAt(mSelectedItem - mBookingsListView.getFirstVisiblePosition());
                        if(item != null){
                            RelativeLayout overlayOptions = (RelativeLayout) item.findViewById(R.id.bookingOptionsOverlay);
                            overlayOptions.setVisibility(View.GONE);
                        }
                    }
                    Animation slideInAnimation = AnimationUtils.loadAnimation(getActivity(),    R.anim.booking_options_overlay_animation);
                    holder.optionsOverlay.startAnimation(slideInAnimation);
                    holder.optionsOverlay.setVisibility(View.VISIBLE);
                    mSelectedItem = position;
                    configureOverlayButtons(holder);
                }
            });

            final Booking booking = mBookings.get(position);
            holder.productName.setText(booking.getName().toUpperCase());
            holder.price.setText("Prix lors de la réservation : " + String.format("%.2f", Float.valueOf(booking.getPrice())) + " €");
            holder.shopName.setText(booking.getShopName());
            holder.endDate.setText(booking.getEndDate());
            holder.productImage.setScaleType(ImageView.ScaleType.CENTER_CROP);

            DisplayImageOptions options = new DisplayImageOptions.Builder()
                    .showImageOnLoading(R.drawable.product_placeholder)
                    .showImageOnFail(R.drawable.product_no_image_placeholder)
                    .cacheInMemory(true)
                    .cacheOnDisk(true)
                    .build();
            ImageLoader imageLoader = ImageLoader.getInstance();
            imageLoader.displayImage(BeeWylApiClient.getImageUrl(booking.getImageURL()),holder.productImage, options);
        }
        if(type == 1){
            holder.placeholderLL.setLayoutParams(BeeWylHelper.getPlaceHolderSizeForFreeScreenSpace(getActivity(),0));
        }
        return convertView;
    }


    private void configureOverlayButtons(final ViewHolder holder){

        holder.cancelBooking.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                AlertDialog.Builder ab = new AlertDialog.Builder(getActivity());
                ab.setMessage("Annuler la réservation ?").setPositiveButton("Oui", dialogClickListener)
                        .setNegativeButton("Non", dialogClickListener).show();
            }
        });

        holder.displayDirections.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                launchMapActivity();
            }
        });

        holder.callShop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                launchDialer();
            }
        });

        holder.discardOverlay.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Animation hideOverlayAnimation = AnimationUtils.loadAnimation(getActivity(), R.anim.booking_overlay_dismiss);
                holder.optionsOverlay.startAnimation(hideOverlayAnimation);
                holder.optionsOverlay.setVisibility(View.GONE);
                holder.optionsOverlay.clearAnimation();
            }
        });
    }


    private void sendCancelBookingToAPI(String id_booking) throws JsonProcessingException {

            BeeWylApiClient.cancelBooking(id_booking, new AsyncHttpResponseHandler() {

                @Override
                public void onSuccess(int i, Header[] headers, byte[] bytes) {
                    try {
                        Log.v("xdebug CANCEL", new String(bytes, "UTF_8"));
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    }
                }
                @Override
                public void onFailure(int i, Header[] headers, byte[] bytes, Throwable throwable) {
                    Log.v("xdebug CANCEL ERROR", String.valueOf(throwable));
                }
            });
    }

    DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            switch (which){
                case DialogInterface.BUTTON_POSITIVE:
                    Animation hideOverlayAnimation = AnimationUtils.loadAnimation(getActivity(), R.anim.booking_overlay_dismiss);
                    mBookingsListView.getChildAt(mSelectedItem-mBookingsListView.getFirstVisiblePosition()).startAnimation(hideOverlayAnimation);
                    new Handler().postDelayed(new Runnable() {
                        public void run() {
                            try {
                                sendCancelBookingToAPI(mBookings.get(mSelectedItem).getId());
                            } catch (JsonProcessingException e) {
                                e.printStackTrace();
                            }
                            mBookings.remove(mSelectedItem);
                            mSelectedItem = -1;
                            updateBookingsListView(mBookings);
                        }
                    }, hideOverlayAnimation.getDuration());
                    break;

                case DialogInterface.BUTTON_NEGATIVE:
                    dialog.cancel();
                    break;
            }
        }
    };       
}

}

And the item inflated :

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="5dp"
            android:paddingLeft="5dp"
            android:paddingRight="5dp"
    >


<LinearLayout
        android:id="@+id/bookingItemLL"
        android:layout_width="match_parent"
        android:layout_height="151dp"
        android:orientation="horizontal"
        android:weightSum="100"
        android:background="@drawable/product_item_rectangle"
        >

    <ImageView
            android:id="@+id/bookedProductImage"
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:background="@android:color/white"
            android:src="@drawable/nivea"
            />


    <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:gravity="center_vertical"
            >
        <TextView
                android:id="@+id/bookingProductName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:text="BRUME NIVEA"
                android:textColor="@color/ProductsBlue"
                android:textSize="16dp"
                android:textStyle="bold"
                />

        <TextView
                android:id="@+id/bookedProductPrice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Prix lors de la réservation : 24,90€"
                android:textSize="12dp"
                android:layout_marginLeft="10dp"
                android:layout_marginTop="5dp"
                android:textColor="@color/ProductsBlue"                        android:layout_gravity="left"
                />

        <TextView
                android:id="@+id/bookingShopName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:layout_marginTop="5dp"
                android:text="Magasin"
                android:textSize="12dp"
                android:textColor="@color/ProductsBlue"
                />

        <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:layout_marginTop="5dp"
                android:text="Réservé jusqu'au"
                android:textSize="12dp"
                android:textColor="@color/ProductsBlue"                        />

        <TextView
                android:id="@+id/bookingEndDate"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:text="-"
                android:textSize="12dp"
                android:textColor="@color/ProductsBlue"                        />
    </LinearLayout>
</LinearLayout>


<RelativeLayout android:id="@+id/bookingOptionsOverlay"
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#EEFFFFFF"
                android:visibility="gone">


    <ImageView
            android:id="@+id/discardOverlay"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_alignParentRight="true"
            android:layout_alignParentTop="true"
            android:src="@drawable/ic_discard_booking_overlay"
            android:padding="5dp"
            />


    <Button android:id="@+id/callShop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="APPELER"
            android:layout_weight="1"
            android:background="#00000000"
            android:drawableTop="@drawable/booking_call"
            android:textColor="@color/ProductsBlue"
            android:textSize="14dp"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:drawablePadding="20dp"
            android:layout_marginLeft="20dp"
            />
    <Button android:id="@+id/cancelBooking"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ANNULER"
            android:layout_weight="1"
            android:background="#00000000"
            android:drawableTop="@drawable/booking_cancel"
            android:textColor="@color/ProductsBlue"
            android:textSize="14dp"
            android:layout_centerInParent="true"
            android:drawablePadding="20dp"

            />
    <Button android:id="@+id/routeShop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ITINERAIRE"
            android:layout_weight="1"
            android:background="#00000000"
            android:drawableTop="@drawable/booking_route"
            android:textColor="@color/ProductsBlue"
            android:textSize="14dp"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:drawablePadding="20dp"
            android:layout_marginRight="20dp"
            />

    </RelativeLayout>

</RelativeLayout>

Solution

  • Your problem comes from re-using the convertView.

    When the previous item got a click the OnClickListener fired and in there the visibility for the item was set to GONE. Later on this same view got recycled and passed to getView() as the convertView. Because you are re-using it without resetting any changes that were made you are now working with a View for a new item that is not in a known state. You should make sure you undo any changes before using a convertView.

    The quick fix is to not re-use the convertView that is passed into getView(). So, in your code where you check if you can re-use the convertView:

    if(convertView == null)
    

    Sabotage that check just to see if things start working:

    if(true)
    

    If that does the trick you will probably want to fix it properly.

    In the else clause of the above check, you are getting the item holder from the tag. Also undo any changes that your OnClickListeners could have made. You want to start with a View for a new item in a known state. You should initialize it explicitly. For example:

    if(convertView == null) {
        // ... snipped all the initialization ...
    } else {
        holder = (ViewHolder)convertView.getTag();
        convertView.setVisibility(View.VISIBLE);
    }
    

    Update

    I have never used a 'heterogenous' adapter so I can't really answer why "the convertView is reusing the overlay View instead of my item's root View." The Android developer documentation for Adapter.getView() says about the convertView argument:

    The old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using. If it is not possible to convert this view to display the correct data, this method can create a new view. Heterogeneous lists can specify their number of view types, so that this View is always of the right type (see getViewTypeCount() and getItemViewType(int)).

    The emphasized bit says that you cannot depend on the system to pass you a convertView of the right type, while the last sentence says the opposite (as I read it).

    Basically, I don't know why it's not working. I guess in the test where you check if you must inflate a new view yourself

    if(convertView == null)
    

    you should also check if it is the right kind of view:

    if(convertView == null || getItemViewTypeFromView(convertView) != type)
    

    Where getItemViewTypeFromView() is something like this:

    private int getItemViewTypeFromView(View view) {
        switch (view.getId()) {
            case R.id.item_layout_root:
                return TYPE_ITEM;
            case R.id.placeholder_layout_root:
                return TYPE_PLACEHOLDER;
            default: 
                throw new UnsupportedOperationException();
        }
    }
    

    In the item and placeholder layouts, give the root elements an id so you distinguish between them. So something like this:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/item_layout_root"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="5dp"
            android:paddingLeft="5dp"
            android:paddingRight="5dp" >
    
        ... snipped the elements that make up the body of the layout ...
    </RelativeLayout>
    

    I haven't tried the above, so I hope it works for you.

    Good luck!