ฉันจะอัปเดตแถวเดียวใน ListView ได้อย่างไร


134

ฉันมีListViewรายการข่าวที่แสดง ประกอบด้วยรูปภาพชื่อและข้อความบางส่วน รูปภาพถูกโหลดในเธรดแยกต่างหาก (โดยมีคิวและทั้งหมด) และเมื่อรูปภาพถูกดาวน์โหลดตอนนี้ฉันเรียกnotifyDataSetChanged()ใช้รายการอะแดปเตอร์เพื่ออัปเดตรูปภาพ วิธีนี้ใช้งานได้ แต่getView()ถูกเรียกบ่อยเกินไปเนื่องจากnotifyDataSetChanged()เรียกgetView()รายการที่มองเห็นได้ทั้งหมด ฉันต้องการอัปเดตเพียงรายการเดียวในรายการ ฉันจะทำอย่างไร

ปัญหาที่ฉันมีกับแนวทางปัจจุบันของฉันคือ:

  1. การเลื่อนทำได้ช้า
  2. ฉันมีภาพเคลื่อนไหวที่เลือนหายไปในภาพซึ่งจะเกิดขึ้นทุกครั้งที่มีการโหลดภาพใหม่ในรายการ

คำตอบ:


200

ฉันพบคำตอบขอบคุณข้อมูลของคุณ Michelle View#getChildAt(int index)แน่นอนคุณจะได้รับมุมมองที่เหมาะสมโดยใช้ สิ่งที่จับได้คือมันเริ่มนับจากรายการแรกที่มองเห็นได้ ในความเป็นจริงคุณจะได้รับรายการที่มองเห็นได้เท่านั้น คุณแก้ปัญหานี้ด้วยListView#getFirstVisiblePosition().

ตัวอย่าง:

private void updateView(int index){
    View v = yourListView.getChildAt(index - 
        yourListView.getFirstVisiblePosition());

    if(v == null)
       return;

    TextView someText = (TextView) v.findViewById(R.id.sometextview);
    someText.setText("Hi! I updated you manually!");
}

2
หมายเหตุถึงตนเอง: mImgView.setImageURI () จะอัปเดตรายการ แต่เพียงครั้งเดียว ดังนั้นฉันจึงควรใช้ mLstVwAdapter.notifyDataSetInvalidated () แทน
kellogs

วิธีนี้ใช้ได้ผล แต่สามารถทำได้เฉพาะกับ yourListView.getChildAt (ดัชนี); และเปลี่ยนมุมมอง เฉลยทั้งคู่ !!
AndroidDev

จะเกิดอะไรขึ้นถ้าฉันเรียก getView () บนอะแด็ปเตอร์เฉพาะเมื่อตำแหน่งอยู่ระหว่าง getFirstVisiblePosition () และ getLastVisiblePosition () มันจะทำงานเหมือนเดิมไหม ฉันคิดว่ามันจะอัปเดตมุมมองตามปกติใช่ไหม
นักพัฒนา Android

ในกรณีของฉันมุมมองบางครั้งเป็นโมฆะเนื่องจากแต่ละรายการกำลังอัปเดตแบบอะซิงโครนัสควรตรวจสอบว่า view! = null
Khawar

2
ใช้งานได้ดี มันช่วยให้ฉันทำงานในการใช้งานฟีเจอร์ Like ในแอพที่เหมือน Instagram ได้สำเร็จ ฉันใช้อะแดปเตอร์ที่กำหนดเองการออกอากาศในปุ่ม like ภายในองค์ประกอบรายการเปิดตัว asynctask เพื่อบอกแบ็กเอนด์เกี่ยวกับสิ่งที่คล้ายกันด้วยเครื่องรับสัญญาณออกอากาศในส่วนหลักและในที่สุดคำตอบของ Erik ในการอัปเดตรายการ
Josh

72

คำถามนี้ถูกถามในงาน Google I / O 2010 คุณสามารถดูได้ที่นี่:

โลกของ ListView เวลา 52:30 น

โดยทั่วไปสิ่งที่ Romain ผู้ชายอธิบายคือการเรียกgetChildAt(int)ในListViewที่จะได้รับการดูและ (ฉันคิด) โทรgetFirstVisiblePosition()ไปหาความสัมพันธ์ระหว่างตำแหน่งและดัชนี

Romain ยังชี้ไปที่โครงการที่เรียกว่าชั้นเป็นตัวอย่างที่ผมคิดว่าเขาอาจหมายถึงวิธีการแต่ฉันไม่สามารถหาการเรียกร้องของShelvesActivity.updateBookCovers()getFirstVisiblePosition()

การอัปเดตที่ยอดเยี่ยมมาจาก:

RecyclerView จะแก้ไขปัญหานี้ในอนาคตอันใกล้ ตามที่ระบุไว้ในhttp://www.grokkingandroid.com/first-glance-androids-recyclerview/คุณจะสามารถเรียกใช้วิธีการเพื่อระบุการเปลี่ยนแปลงได้อย่างถูกต้องเช่น:

void notifyItemInserted(int position)
void notifyItemRemoved(int position)
void notifyItemChanged(int position)

นอกจากนี้ทุกคนจะต้องการใช้มุมมองใหม่ที่ใช้ RecyclerView เนื่องจากพวกเขาจะได้รับรางวัลเป็นภาพเคลื่อนไหวที่ดูสวยงาม! อนาคตน่าดู! :-)


3
สิ่งที่ดี! :) บางทีคุณสามารถเพิ่มการตรวจสอบค่าว่างที่นี่ได้เช่นกัน - v อาจเป็นโมฆะหากมุมมองไม่พร้อมใช้งานในขณะนี้ และแน่นอนว่าข้อมูลนี้จะหายไปหากผู้ใช้เลื่อน ListView ดังนั้นควรอัปเดตข้อมูลในอแด็ปเตอร์ด้วย (อาจไม่ต้องเรียกใช้ alertDataSetChanged ()) โดยทั่วไปฉันคิดว่ามันเป็นความคิดที่ดีที่จะเก็บตรรกะทั้งหมดนี้ไว้ในอะแด็ปเตอร์นั่นคือการส่งการอ้างอิง ListView ไป
mreichelt

สิ่งนี้ใช้ได้ผลสำหรับฉันยกเว้น - เมื่อมุมมองออกจากหน้าจอเมื่อกลับเข้าสู่การเปลี่ยนแปลงจะถูกรีเซ็ต ฉันเดาว่าเนื่องจากใน getview ยังคงได้รับข้อมูลเดิม ฉันจะหลีกเลี่ยงสิ่งนี้ได้อย่างไร?
gloscherrybomb

6

นี่คือวิธีที่ฉันทำ:

รายการของคุณ (แถว) ต้องมีรหัสที่ไม่ซ้ำกันเพื่อให้คุณสามารถอัปเดตได้ในภายหลัง ตั้งค่าแท็กของทุกมุมมองเมื่อรายการรับมุมมองจากอะแดปเตอร์ (คุณยังสามารถใช้คีย์แท็กได้หากใช้แท็กเริ่มต้นที่อื่น)

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
    View view = super.getView(position, convertView, parent);
    view.setTag(getItemId(position));
    return view;
}

สำหรับการอัปเดตให้ตรวจสอบทุกองค์ประกอบของรายการหากมีมุมมองที่มีรหัสที่ระบุอยู่จะปรากฏให้เห็นดังนั้นเราจึงทำการอัปเดต

private void update(long id)
{

    int c = list.getChildCount();
    for (int i = 0; i < c; i++)
    {
        View view = list.getChildAt(i);
        if ((Long)view.getTag() == id)
        {
            // update view
        }
    }
}

มันง่ายกว่าวิธีอื่นจริง ๆ และดีกว่าเมื่อคุณจัดการกับรหัสไม่ใช่ตำแหน่ง! นอกจากนี้คุณต้องเรียกอัปเดตสำหรับรายการที่มองเห็นได้


3

รับคลาสโมเดลก่อนเป็นโกลบอลเหมือนกับอ็อบเจ็กต์คลาสโมเดลนี้

SampleModel golbalmodel=new SchedulerModel();

และเริ่มต้นเป็นโกลบอล

รับแถวปัจจุบันของมุมมองตามแบบจำลองโดยการเริ่มต้นเป็นโมเดลส่วนกลาง

SampleModel data = (SchedulerModel) sampleList.get(position);
                golbalmodel=data;

ตั้งค่าที่เปลี่ยนแปลงไปเป็น global model object method ที่จะตั้งค่าและเพิ่ม alertDataSetChanged มันใช้งานได้สำหรับฉัน

golbalmodel.setStartandenddate(changedate);

notifyDataSetChanged();

นี่คือคำถามที่เกี่ยวข้องกับเรื่องนี้พร้อมคำตอบที่ดี


นี่เป็นแนวทางที่ถูกต้องเนื่องจากคุณได้รับออบเจ็กต์ที่คุณต้องการอัปเดตอัปเดตแล้วแจ้งอแด็ปเตอร์และนั่นจะเปลี่ยนข้อมูลแถวด้วยข้อมูลใหม่
GastónSaillén

2

คำตอบนั้นชัดเจนและถูกต้องฉันจะเพิ่มแนวคิดสำหรับCursorAdapterกรณีที่นี่

หากคุณเป็นคลาสย่อยCursorAdapter(หรือResourceCursorAdapterหรือSimpleCursorAdapter) คุณจะได้รับการนำไปใช้ViewBinderหรือลบล้างbindView()และnewView()วิธีการเหล่านี้จะไม่ได้รับดัชนีรายการปัจจุบันในอาร์กิวเมนต์ ดังนั้นเมื่อข้อมูลบางส่วนมาถึงและคุณต้องการอัปเดตรายการที่มองเห็นได้ที่เกี่ยวข้องคุณจะทราบดัชนีได้อย่างไร

วิธีแก้ปัญหาของฉันคือ:

  • เก็บรายการมุมมองรายการที่สร้างขึ้นทั้งหมดเพิ่มรายการในรายการนี้จาก newView()
  • เมื่อข้อมูลมาถึงให้ทวนซ้ำและดูว่าข้อมูลใดที่ต้องอัปเดต - ดีกว่าทำnotifyDatasetChanged()และรีเฟรชข้อมูลทั้งหมด

เนื่องจากการดูการรีไซเคิลจำนวนการอ้างอิงมุมมองฉันจะต้องจัดเก็บและการทำซ้ำจะเท่ากับจำนวนรายการที่มองเห็นได้บนหน้าจอโดยประมาณ


2
int wantedPosition = 25; // Whatever position you're looking for
int firstPosition = linearLayoutManager.findFirstVisibleItemPosition(); // This is the same as child #0
int wantedChild = wantedPosition - firstPosition;

if (wantedChild < 0 || wantedChild >= linearLayoutManager.getChildCount()) {
    Log.w(TAG, "Unable to get view for desired position, because it's not being displayed on screen.");
    return;
}

View wantedView = linearLayoutManager.getChildAt(wantedChild);
mlayoutOver =(LinearLayout)wantedView.findViewById(R.id.layout_over);
mlayoutPopup = (LinearLayout)wantedView.findViewById(R.id.layout_popup);

mlayoutOver.setVisibility(View.INVISIBLE);
mlayoutPopup.setVisibility(View.VISIBLE);

สำหรับ RecycleView โปรดใช้รหัสนี้


ฉันใช้สิ่งนี้บน recycleView มันใช้งานได้ แต่เมื่อคุณเลื่อนรายการจะมีการเปลี่ยนแปลงอีกครั้ง ความช่วยเหลือใด ๆ
Fahid Nadeem

1

ฉันใช้รหัสที่ให้ Erik ใช้งานได้ดี แต่ฉันมีอะแดปเตอร์แบบกำหนดเองที่ซับซ้อนสำหรับมุมมองรายการของฉันและฉันต้องเผชิญกับการติดตั้งโค้ดสองครั้งที่อัปเดต UI ฉันพยายามรับมุมมองใหม่จากเมธอด getView ของอะแด็ปเตอร์ของฉัน (รายการอาร์เรย์ที่เก็บข้อมูล listview ได้รับการอัปเดต / เปลี่ยนแปลงทั้งหมดแล้ว):

View cell = lvOptim.getChildAt(index - lvOptim.getFirstVisiblePosition());
if(cell!=null){
    cell = adapter.getView(index, cell, lvOptim); //public View getView(final int position, View convertView, ViewGroup parent)
    cell.startAnimation(animationLeftIn());
}

มันใช้งานได้ดี แต่ฉันไม่รู้ว่านี่เป็นแนวทางปฏิบัติที่ดีหรือไม่ ดังนั้นฉันไม่จำเป็นต้องติดตั้งโค้ดที่อัปเดตรายการสองครั้ง


1

ฉันใช้สิ่งนี้

private void updateSetTopState(int index) {
        View v = listview.getChildAt(index -
                listview.getFirstVisiblePosition()+listview.getHeaderViewsCount());

        if(v == null)
            return;

        TextView aa = (TextView) v.findViewById(R.id.aa);
        aa.setVisibility(View.VISIBLE);
    }

ฉันได้รับ View as Relative layout วิธีค้นหา Textview ภายในเลย์เอาต์สัมพัทธ์
Girish

1

ฉันสร้างโซลูชันอื่นเช่นเมธอดRecyclyerViewเป็นโมฆะnotifyItemChanged(int position)สร้างคลาสCustomBaseAdapterเช่นนี้:

public abstract class CustomBaseAdapter implements ListAdapter, SpinnerAdapter {
    private final CustomDataSetObservable mDataSetObservable = new CustomDataSetObservable();

    public boolean hasStableIds() {
        return false;
    }

    public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }

    public void unregisterDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.unregisterObserver(observer);
    }

    public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

    public void notifyItemChanged(int position) {
        mDataSetObservable.notifyItemChanged(position);
    }

    public void notifyDataSetInvalidated() {
        mDataSetObservable.notifyInvalidated();
    }

    public boolean areAllItemsEnabled() {
        return true;
    }

    public boolean isEnabled(int position) {
        return true;
    }

    public View getDropDownView(int position, View convertView, ViewGroup parent) {
        return getView(position, convertView, parent);
    }

    public int getItemViewType(int position) {
        return 0;
    }

    public int getViewTypeCount() {
        return 1;
    }

    public boolean isEmpty() {
        return getCount() == 0;
    } {

    }
}

อย่าลืมสร้างคลาส CustomDataSetObservableสำหรับmDataSetObservableตัวแปรในCustomAdapterClassเช่นนี้:

public class CustomDataSetObservable extends Observable<DataSetObserver> {

    public void notifyChanged() {
        synchronized(mObservers) {
            // since onChanged() is implemented by the app, it could do anything, including
            // removing itself from {@link mObservers} - and that could cause problems if
            // an iterator is used on the ArrayList {@link mObservers}.
            // to avoid such problems, just march thru the list in the reverse order.
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

    public void notifyInvalidated() {
        synchronized (mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onInvalidated();
            }
        }
    }

    public void notifyItemChanged(int position) {
        synchronized(mObservers) {
            // since onChanged() is implemented by the app, it could do anything, including
            // removing itself from {@link mObservers} - and that could cause problems if
            // an iterator is used on the ArrayList {@link mObservers}.
            // to avoid such problems, just march thru the list in the reverse order.
            mObservers.get(position).onChanged();
        }
    }
}

ในคลาสCustomBaseAdapterมีวิธีการnotifyItemChanged(int position)และคุณสามารถเรียกใช้เมธอดนั้นได้เมื่อคุณต้องการอัปเดตแถวทุกที่ที่คุณต้องการ (จากการคลิกปุ่มหรือที่ใดก็ได้ที่คุณต้องการเรียกใช้เมธอดนั้น) และ voila! แถวเดียวของคุณจะอัปเดตทันที ..


0

วิธีแก้ปัญหาของฉัน: หากถูกต้อง * ให้อัปเดตข้อมูลและรายการที่สามารถดูได้โดยไม่ต้องวาดใหม่ทั้งรายการ ที่อื่น informDataSetChanged

ถูกต้อง - oldData size == ขนาดข้อมูลใหม่และ ID ข้อมูลเก่าและลำดับ == รหัสข้อมูลใหม่และลำดับ

อย่างไร:

/**
 * A View can only be used (visible) once. This class creates a map from int (position) to view, where the mapping
 * is one-to-one and on.
 * 
 */
    private static class UniqueValueSparseArray extends SparseArray<View> {
    private final HashMap<View,Integer> m_valueToKey = new HashMap<View,Integer>();

    @Override
    public void put(int key, View value) {
        final Integer previousKey = m_valueToKey.put(value,key);
        if(null != previousKey) {
            remove(previousKey);//re-mapping
        }
        super.put(key, value);
    }
}

@Override
public void setData(final List<? extends DBObject> data) {
    // TODO Implement 'smarter' logic, for replacing just part of the data?
    if (data == m_data) return;
    List<? extends DBObject> oldData = m_data;
    m_data = null == data ? Collections.EMPTY_LIST : data;
    if (!updateExistingViews(oldData, data)) notifyDataSetChanged();
    else if (DEBUG) Log.d(TAG, "Updated without notifyDataSetChanged");
}


/**
 * See if we can update the data within existing layout, without re-drawing the list.
 * @param oldData
 * @param newData
 * @return
 */
private boolean updateExistingViews(List<? extends DBObject> oldData, List<? extends DBObject> newData) {
    /**
     * Iterate over new data, compare to old. If IDs out of sync, stop and return false. Else - update visible
     * items.
     */
    final int oldDataSize = oldData.size();
    if (oldDataSize != newData.size()) return false;
    DBObject newObj;

    int nVisibleViews = m_visibleViews.size();
    if(nVisibleViews == 0) return false;

    for (int position = 0; nVisibleViews > 0 && position < oldDataSize; position++) {
        newObj = newData.get(position);
        if (oldData.get(position).getId() != newObj.getId()) return false;
        // iterate over visible objects and see if this ID is there.
        final View view = m_visibleViews.get(position);
        if (null != view) {
            // this position has a visible view, let's update it!
            bindView(position, view, false);
            nVisibleViews--;
        }
    }

    return true;
}

และแน่นอนว่า:

@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
    final View result = createViewFromResource(position, convertView, parent);
    m_visibleViews.put(position, result);

    return result;
}

ละเว้นพารามิเตอร์สุดท้ายของ bindView (ฉันใช้มันเพื่อพิจารณาว่าฉันจำเป็นต้องรีไซเคิลบิตแมปสำหรับ ImageDrawable หรือไม่)

ดังที่ได้กล่าวไว้ข้างต้นจำนวนมุมมองที่ 'มองเห็นได้' ทั้งหมดเป็นจำนวนโดยประมาณที่พอดีกับหน้าจอ (ไม่สนใจการเปลี่ยนแปลงการวางแนว ฯลฯ ) ดังนั้นจึงไม่ต้องใช้หน่วยความจำขนาดใหญ่


0

นอกเหนือจากโซลูชันนี้ ( https://stackoverflow.com/a/3727813/5218712 ) เพียงแค่ต้องการเพิ่มว่าควรใช้งานได้ก็ต่อเมื่อlistView.getChildCount() == yourDataList.size(); มีมุมมองเพิ่มเติมภายใน ListView

ตัวอย่างวิธีการเติมองค์ประกอบลูก:  listView.mChildren อาร์เรย์

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.