Question

I'm using an ArrayAdapter for a list of my own type of objects (only one type) and I give the user an option to create more items (thus creating more views for those items). At some point, getView sent a new "position" index with a non-null "convertView". It then shows the first view in the last position. After that, when scrolling the views get all mixed up. I'm assuming this means I manipulated the views in ways I shouldn't have but I just don't see where. Here is some code:

    @Override
public View getView(int position, View convertView, ViewGroup parent) {
    View v;
    PreviewItemHolder holder = null;

    // Initialize view if convertview is null
    if (convertView == null) {
        v = newView(parent, position);
    }
    // Populate from previously saved holder
    else {
        // Use previous item if not null
        v = convertView;
    }

    // Populate if the holder is null (newly inflated view) OR
    // if current view's holder's flag is true and requires populating
    if ((holder == null) || (holder.readPopulateFlag())) {
        bindView(position, v);
    }

    return v;
}

    private View newView(ViewGroup parent, int position) {
    // Getting view somehow...
    LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View inflatedView = inflater.inflate(R.layout.preview_element_set, parent, false);
    PreviewItemHolder holder = new PreviewItemHolder();

    holder.set = (Set) mSets.get(position);
    holder.previewElementHolders = new ArrayList<PreviewElementHolder>();
    holder.expandArea = (View) inflatedView.findViewById(R.id.expandArea);
    holder.repetitionsLabel = (TextView) inflatedView.findViewById(R.id.previewRepetitionsInput);
    holder.endlessInput = (CheckBox) inflatedView.findViewById(R.id.previewSetEndlessInput);
    holder.nameLabel = (TextView) inflatedView.findViewById(R.id.previewSetNameLabel);
    holder.commentInput = (EditText) inflatedView.findViewById(R.id.previewSetCommentInput);
    holder.soundInput = (EditText) inflatedView.findViewById(R.id.previewSetSoundInput);
    holder.addElementButton = (Button) inflatedView.findViewById(R.id.previewSetAddElements);
    holder.expand = (View) inflatedView.findViewById(R.id.infoArea);
    holder.collapse = (View) inflatedView.findViewById(R.id.collapse);

    final int setsLength = holder.set.getElements().size();

    for (int i = 0; i < setsLength; i++) {
        AElement currElement = holder.set.getElements().get(i);

        // Creating new element holder according to the type
        if (currElement instanceof Rest) {
            holder.previewElementHolders.add(new PreviewRestHolder());
        }
        else if (currElement instanceof TimeExercise) {
            holder.previewElementHolders.add(new PreviewTimeExerciseHolder());
        }
        else if (currElement instanceof RepetitionExercise) {
            holder.previewElementHolders.add(new PreviewRepetitionExerciseHolder());
        }

        View currLayout = inflateElement(currElement, inflater, i, holder.previewElementHolders.get(i));

        // Add the child before the hairline, collapse image and the add
        // button
        // (3 last children of the expandArea view
        ((ViewGroup) holder.expandArea).addView(currLayout, ((ViewGroup) holder.expandArea).getChildCount() - CHILDREN_INDEX_AFTER_PHASES_LABEL);
    }

    inflatedView.setTag(holder);

    return inflatedView;
}

private void bindView(int position, View inflatedView) {
    final PreviewItemHolder holder = (PreviewItemHolder) inflatedView.getTag();
    holder.set.setId(position);
    holder.endlessInput.setChecked(holder.set.getEndless());
    holder.soundInput.setText(holder.set.getSound());
    holder.nameLabel.setText(holder.set.getName());
    holder.commentInput.setText(holder.set.getComment());

    // Make sure there is a name. If none, put default
    if (holder.nameLabel.getText().equals("")) {
        holder.nameLabel.setText(R.string.default_set_name);
    }

    // Set repetitions value according to the endless flag
    if (holder.set.getEndless()) {
        holder.repetitionsLabel.setText(R.string.infinity);
    }
    else {
        holder.repetitionsLabel.setText(String.valueOf(holder.set.getRepetitions()));
    }

    // Set click listeners
    holder.endlessInput.setOnCheckedChangeListener(new OnCheckedChangeListener() {

        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

            // Save endless flag
            holder.set.setEndless(isChecked);

            // If an endless set - Dropset
            if (isChecked) {
                holder.repetitionsLabel.setText(R.string.infinity);
            }
            else {
                // Regular set
                holder.repetitionsLabel.setText(String.valueOf(holder.set.getRepetitions()));
            }

            hideShowRepsWeights(holder);
        }

    });

    holder.repetitionsLabel.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {

            NumericDialog instance = NumericDialog.newInstance(holder, holder.set, NumericDialog.INTEGER_MODE, Consts.SET_REPETITIONS_METHOD_NAME);
            instance.show(((Activity) getContext()).getFragmentManager(), null);
        }
    });

    holder.nameLabel.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            // Setting flag to true to allow populating this view
            holder.rePopulateFlag = true;
            SetNameDialog instance = SetNameDialog.newInstance(holder.set);
            instance.show(((Activity) getContext()).getFragmentManager(), null);
        }
    });

    holder.commentInput.setOnFocusChangeListener(new OnFocusChangeListener() {
        @Override
        public void onFocusChange(View v, boolean hasFocus) {
            if (!hasFocus) {
                // After focus is lost, save the text into the set
                holder.set.setComment(holder.commentInput.getText().toString());
            }
        }
    });

    // TODO Change that into a dialog that allows selection of sounds
    holder.soundInput.setOnFocusChangeListener(new OnFocusChangeListener() {
        @Override
        public void onFocusChange(View v, boolean hasFocus) {
            if (!hasFocus) {
                // After focus is lost, save the text into the set
                holder.set.setSound(holder.soundInput.getText().toString());
            }
        }
    });

    holder.expand.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            // Change visibility - Show expandArea and its data
            holder.expandArea.setVisibility(View.VISIBLE);
            holder.expand.setVisibility(View.GONE);
            holder.collapse.setVisibility(View.VISIBLE);
        }
    });

    holder.collapse.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            // Change visibility - Hide expandArea and its data
            holder.expandArea.setVisibility(View.GONE);
            holder.collapse.setVisibility(View.GONE);
            holder.expand.setVisibility(View.VISIBLE);
        }
    });

    holder.addElementButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            AddElementDialog instance = AddElementDialog.newInstance(holder);
            instance.show(((Activity) getContext()).getFragmentManager(), null);
        }
    });

    // Populate elements
    for (PreviewElementHolder elementHolder : holder.previewElementHolders) {
        populateElement(elementHolder, holder);
    }

    // Finally hide/show if needed - Should this be put somewere else?
    hideShowRepsWeights(holder);
}

Please tell me if you think I should upload more methods to make things clearer.

Was it helpful?

Solution

A friend explained the problem to me and now it seems to work. Basically ListView only holds a small number of views and recycles them all the time. In my case I have a Nexus 4 and so it seems to have 7 views total because the 8th was always the one who started to cause trouble. What I was missing in my getView() was a condition checking for correlation between the position and the ID of the current item within the ArrayAdapter. Here is how it looks now that it works:

@Override
public View getView(int position, @Nullable View convertView, ViewGroup parent) {
    View v;
    PreviewItemHolder holder = null;

    // Initialize view if convertview is null
    if (convertView == null) {
        v = newView(parent, position);
    }
    // Populate from previously saved holder
    else {
        // If position and id of set do not match, this view needs to be re-created, not recycled
        if (((PreviewItemHolder) convertView.getTag()).set.getId() != position) {
            v = newView(parent, position);
        }
        else {
            // Use previous item if not null
            v = convertView;

            // Get holder
            holder = (PreviewItemHolder) v.getTag();
        }
    }

    // Populate if the holder is null (newly inflated view) OR
    // if current view's holder's flag is true and requires populating
     if (holder == null || holder.readPopulateFlag()) {
         bindView(position, v);
     }

    return v;
}

OTHER TIPS

You need to call bindView() always. The idea or reuse is as following. If convertView is null, you create and initialize a new view. If convertView is not null, you take this view and convert it to be new view, meaning you call bindView() with convertView instance.

Checkout this Javadoc for more details.

When you have several types of views that should be recycled you have to tell the list view how many types you have got by implement the method

@Override
    public int getViewTypeCount() {
        return 7;
    }
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top