Add image to ListView custom Android

Chào anh chị. Em có bài tập java và e bị 1 chỗ không biết xử lý s. Em có 1 listview và 1 custom adaptep(Em sẽ gọi là arrayAdapterNation nó chứa 1 object Nation trong đó). Trong Adapter em load ảnh từ url về. Và mỗi khi em scroll thì nó liên tục thay đổi ảnh cho tới khi em dừng 1 chỗ. E không biết xử lý sao cả. Anh chị cho em giải pháp với ạ. E đang dùng AsyncTask để load ảnh về. Anh chị làm theo hướng của em. Hoặc là mới e sẽ tiếp nhận.

    package com.example.nationinfo;

    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.os.AsyncTask;
    import android.util.Log;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.BaseAdapter;
    import android.widget.ImageView;
    import android.widget.TextView;

    import java.io.IOException;
    import java.io.InputStream;
    import java.net.HttpURLConnection;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.util.ArrayList;

    public class ArrayAdapterNation extends BaseAdapter {
        ArrayList<Nation> arrayList;
        int layout;
        Context context;

    public ArrayAdapterNation(ArrayList<Nation> arrayList, int layout, Context context) {
        this.arrayList = arrayList;
        this.layout = layout;
        this.context = context;
    }

    @Override
    public int getCount() {
        return arrayList.size();
    }

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

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

    private class ViewHolder{
        TextView nameNation;
        ImageView imageViewNation;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final ViewHolder viewHolder;
        if(convertView == null){
            viewHolder = new ViewHolder();
            LayoutInflater inflater = (LayoutInflater) context.getSystemService(context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(layout,null, false);
            viewHolder.nameNation = convertView.findViewById(R.id.nationName);
            viewHolder.imageViewNation = convertView.findViewById(R.id.imageViewNation);
            convertView.setTag(viewHolder);
        }else{
            viewHolder = (ViewHolder) convertView.getTag();
        }
        viewHolder.nameNation.setText(arrayList.get(position).getCountryName());
        String urlImage = "https://img.geonames.org/flags/x/"+arrayList.get(position).getCountryCode()+".gif";

        if(viewHolder.imageViewNation != null){
            new AsyncTask<String, Void, Bitmap>(){

                @Override
                protected Bitmap doInBackground(String... strings) {

                    URL url = null;
                    try {
                        url = new URL(strings[0]);
                    } catch (MalformedURLException e) {
                        e.printStackTrace();
                    }
                    HttpURLConnection connection = null;
                    try {
                        connection = (HttpURLConnection) url.openConnection();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    connection.setDoInput(true);
                    try {
                        connection.connect();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    InputStream input = null;
                    try {
                        input = connection.getInputStream();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    Bitmap myBitmap = BitmapFactory.decodeStream(input);
                    return myBitmap;
                }

                @Override
                protected void onPostExecute(Bitmap result) {
                    if(result != null){
                        viewHolder.imageViewNation.setImageBitmap(result);
                    }
                }
            }.execute(urlImage);
        }

        return convertView;
    }
}

Kết quả ra sao bạn? Nếu bạn làm thế thì nó sẽ gọi đúng các vị trí khi bạn cuộn qua và sẽ tải bất đồng bộ thôi.

3 Likes

Nó load ảnh lung tung xong đợi 1 lúc nó sẽ load đúng. Mình biết là tại từng vị trí khác nhau thì nó sẽ render cái ảnh ra tại ví trị đó. Thí dụ vị trí list item [1,2, 3, 4, 5], kéo 1 tí xuống [5, 6, 7, 8, 9] thì tại đây thì nó sẽ load ảnh tại vị trí 1 lần lượt là 2 3 4 rồi tới 5. Do cái asynctask. Mình nghĩ thôi. Chứ không biết đúng không. Cũng không biết cách giải quyết s cho đúng nữa


Tham khảo vd này, giờ này ko ai dùng ListView nữa đâu, thay vào cái list chứa url bạn muốn load là xong.
À mà quên code bạn chưa đúng vì ở onPostExecute bạn chưa notify data change.

4 Likes

@Reborn câu trả lời của bạn sai bét rồi, rất nguy hiểm khi quăng 1 câu trả lời ăn sẵn và chứa thông tin sai như bạn. Cả bạn và bạn đặt câu hỏi đều không hiểu cách mà các method của ListView hoạt động. Sẽ rất nguy hiểm nếu chỉ dùng mà không hiểu bản chất sự việc

Quay trở lại vấn đề của bạn đặt câu hỏi, bạn này đang sử dụng 1 pattern phổ biến khi dùng với listview hồi chưa có recycleview. Ý tưởng tương tự cách mà RecycleView hoạt động là cố gắng cache View item, hạn chế inflate View từ XML hay new View ở hàm getView. Chính vì View được cache và dùng lại do đó khi bạn kéo các item trong list view thì sẽ có trường hợp nhiều AsyncTask cùng load ảnh vào cùng 1 image view. Do AsyncTask load ảnh từ network sẽ mất chút thời gian do đó bạn sẽ thấy ảnh được load dần dần thay thế cho nhau vào cùng 1 item trong list view, khi không kéo qua lại nữa thì sẽ thấy ổn định trở lại.
@Reborn

À mà quên code bạn chưa đúng vì ở onPostExecute bạn chưa notify data change.

Câu này của bạn cực kì tai hại do hiểu sai method notifyDataChanged dùng để làm gì. Method đó dùng để thông báo cho ListView biết là DATA trong ListView có sự thay đổi nên cần phải refresh lại để cập nhật. Còn ở đây thì bạn kia đang reference trực tiếp đến 1 ViewItem cụ thể nên có thể setImage trực tiếp, việc gọi method trên là không có ý nghĩa, ngược lại có thể gây loop xử lí

5 Likes

Thế bạn có biết notifyDataChange sẽ gọi trên thread nào ko? và vì sao lại cần gọi ko? bạn đang download ảnh ở nhiều thread khác nhau, nếu bạn ko notify ddeeerr listview reload lại data trên main thread thì ko bao giờ data show đúng được, case này mấy nưm trước cũng nhiều bạn trong team bị nên mình mới biết và hướng dẫn cách giải quyết.
Còn ListView thật sự là từ hồi 2017 là đã gần như rất ít dùng đến, vì Recycler đã được áp dụng cùng mục đích nhưng nhiều ưu điểm hơn.
ANW tuỳ bạn thớt quyết định, thực tế apply thử sẽ hiểu.

4 Likes

thế bạn có biết notifyDataChange nó dùng để làm gì không? Bạn có phân biệt được đâu là phần data và đâu là phần view trong ListView không? Đâu là phần khi thay đổi thì cần notify và đâu là phần không cần không? Ở onPostExecute bạn đó set image thì đâu còn dính dáng gì đến multi thread. Nếu thật sự do không gọi notifyDataChange thì nó chẳng có chuyện như mô tả bên dưới này đâu bạn mà nó sẽ đứng im không thấy thay đổi ảnh gì hết á

PS: ListView hiện nay được dùng ẩn sau 1 số control của Android còn chủ yếu khi dùng về list hoặc grid nhiều dev đã chuyển sang dùng RecyclerView. Tuy nhiên việc hiểu bản chất vấn đề vẫn là cốt lõi chứ không phải dùng cái gì.
PS2: Dùng recyclerview nếu xử lí không tốt vẫn bị lỗi này y như dùng listview mà thôi
Bonus thêm cho bài viết này để hiểu hơn về cơ chế cache item của listview

3 Likes

Giờ m toàn dùng glide để load ảnh thôi, nói chung load ảnh, cache ảnh, xử lý multi thread, multi http connection, cancel http connection khá lằng nhằng nên cái gì có sẵn thì cứ thế mà dùng, biết nguyên lý là đc.
Về ListView với RecyclerView thì sài RecyclerView thôi, google android guide bỏ luôn phần ListView rồi :smile:

3 Likes

Thôi thì tùy bạn vậy mình ko tranh luận nữa, vì bạn đang hiểu sai bản chất notifyDataChange là chỉ gọi để thông báo data có thay đổi, mà data ở đaya ko đơn thuần chỉ là cái array chứa ảnh đâu.
Notify cho adapter biết toàn bộ view item của nó đã có thay đổi CẢ về UI lẫn data.
Mình dựa trên nguyên tắc làm việc thật để nói chớ ko cứ lý thuyết xuông, và mình đã có ví dụ hẵn hoi hoạt động tốt.

4 Likes

OK. Bạn cứ thử copy ví dụ của bạn kia vào project rồi test xem. Bật debug tool lên xem nó sẽ cực kì funny. Nếu cần thì cho log vào method getView xem nó ra cái gì. Có thể hoàn toàn dự đoán được kết quả nếu bạn hiểu rõ bản chất thay vì dùng 1 cách bừa bãi. Có thể nhìn trên giao diện sẽ thấy nó có vẻ fix được vấn đề tuy nhiên nó sinh ra 1 vấn đề khác tệ hơn nhiều.
PS: bạn nên check lại source code của hàm notifyDatasetChanged để biết chính xác nó làm gì với ListView thay vì tư duy sai lầm như vậy.
PS2: bạn có từng thắc mắc vì sao các thư viện load ảnh không có thông tin gì về listview hay recyclerview để mà notify khi load ảnh xong nhưng UI vẫn update không. Vậy thì thực sự method notifyDatasetChanged có phải key để giải quyết vấn đề này không hay là che giấu vấn đề của bạn

3 Likes

Thế mọi người ơi, hướng giải quyết nào cho vấn đề của em vậy? Cho em một số ít thông tin để em có thể fix cũng như là học hỏi. Em không rành lắm.

Hình như mình cũng bị như bạn.
Tham số trong phương thức getView() không như mong đợi.

  • View convertView: sẽ truyền vào khung xem tương ứng với vị trí được hiển thị. Nếu cuộn xuống vị trí 5 -> 10 thì các lần truyền vào vẫn là các khung từ 0 -> 4 chứ không phải là 5 -> 10.

Cách giải quyết của mình là. Tạo tất cả khung xem sẵn ngay khi các mục (item) được thêm vào. Khi getView() được gọi thì cần lấy và trả về khung xem theo tham số position.
Trường hợp của bạn là ngay trong hàm dựng ArrayAdapterNation().
Đoạn mã của mình:

    private static class FileAdapter extends ArrayAdapter<File>{
        
        private static int DIR_COL = 0x22ffff44;
        private static int FILE_COL = 0x2200ff00;
        private static int FILE_COL_SELECTED = 0x444488ff;
        
        private ArrayList<View> va;
        
        private FileAdapter(Context ctx){
            super(ctx, 0);
            va = new ArrayList<>();
        }
        
        @Override
        public void clear(){
            va.clear();
            super.clear();
        }
        
        @Override
        public void insert(File f, int i){
            va.add(i, newView(f));
            super.insert(f, i);
        }
        
        public void addParent(File f){
            View v = newView(f);
            ImageView img = v.findViewById(R.id.image);
            img.setImageResource(R.drawable.back);
            TextView n = v.findViewById(R.id.name);
            n.setText("...");
            va.add(0, v);
            super.insert(f, 0);
        }
        
        @Override
        public void add(File f){
            va.add(newView(f));
            super.add(f);
        }
        
        private View newView(File f){
            View v = LayoutInflater.from(getContext()).inflate(R.layout.file_item, null, false);
            ImageView img = v.findViewById(R.id.image);
            TextView t = v.findViewById(R.id.name);
            t.setText(f.getName());
            t.setSingleLine(true);
            if(f.isDirectory()){
                img.setImageResource(R.drawable.folder);
                v.setBackgroundColor(DIR_COL);
            }else if(f.isFile()){
                img.setImageResource(R.drawable.file);
                v.setBackgroundColor(FILE_COL);
            }
            v.setTag(f);
            return v;
        }
        
        @Override
        public View getView(int p, View v, ViewGroup g) {
           v = va.get(p);
           return v;
        }
        
        public void toggleSelect(int i){
            setSelected(i, !getSelected(i));
        }
        
        public void selectAll(boolean ss){
            for(int i = 0; i < va.size(); ++i){
                if(getItem(i).isDirectory()){
                    continue;
                }
                setSelected(i, ss);
            }
        }
        public void selectAll(){
            selectAll(true);
        }
        
        public void deselect(){
            selectAll(false);
        }
        
        public void setSelected(int p, boolean s){
            if(getItem(p).isFile()){
                va.get(p).setBackgroundColor(s ? FILE_COL_SELECTED : FILE_COL);
            }
        }
        
        public void setSelectedRange(int s, int e, boolean ss){
            for(int i = s; i <= e; ++i){
                setSelected(i, ss);
            }
        }
        
        public void setSelectedRange(){
            int s = -1;
            int e = -1;
            for(int i = 0; i < va.size(); ++i){
                if(getSelected(i)){
                    if(s == -1){
                        s = i;
                    }
                    e = i;
                }
            }
            if(s == -1 || e == -1){
                return;
            }
            setSelectedRange(s, e, true);
        }
        
        public boolean getSelected(int i){
            return getBackgroundColor(va.get(i)) == FILE_COL_SELECTED;
        }
        
        public ArrayList<File> getSelectedFile(){
            ArrayList<File> fs = new ArrayList<>();
            for(int i = 0; i < va.size(); ++i){
                if(getBackgroundColor(va.get(i)) == FILE_COL_SELECTED){
                    fs.add(getItem(i));
                }
            }
            return fs;
        }
        
        public static int getBackgroundColor(View view) {
            Drawable drawable = view.getBackground();
            if (drawable instanceof ColorDrawable) {
                ColorDrawable colorDrawable = (ColorDrawable) drawable;
                if (Build.VERSION.SDK_INT >= 11) {
                    return colorDrawable.getColor();
                }
                try {
                    Field field = colorDrawable.getClass().getDeclaredField("mState");
                    field.setAccessible(true);
                    Object object = field.get(colorDrawable);
                    field = object.getClass().getDeclaredField("mUseColor");
                    field.setAccessible(true);
                    return field.getInt(object);
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            return 0;
        }
    }

Đoạn cần lưu ý:

        // ...
        private ArrayList<View> va;
        // ...
        @Override
        public void add(File f){
            va.add(newView(f)); // <-- Tạo mới
            super.add(f);
        }
        
        private View newView(File f){
            View v = LayoutInflater.from(getContext()).inflate(R.layout.file_item, null, false);
            ImageView img = v.findViewById(R.id.image);
            TextView t = v.findViewById(R.id.name);
            t.setText(f.getName());
            t.setSingleLine(true);
            if(f.isDirectory()){
                img.setImageResource(R.drawable.folder);
                v.setBackgroundColor(DIR_COL);
            }else if(f.isFile()){
                img.setImageResource(R.drawable.file);
                v.setBackgroundColor(FILE_COL);
            }
            v.setTag(f);
            return v;
        }
        
        @Override
        public View getView(int p, View v, ViewGroup g) {
           // v = convertView
           v = va.get(p); // <-- Lấy và trả về từ thực thể đã tạo sẵn.
           return v;
        }
        // ...

Mình có cho phép thêm và xóa (sạch) chứ không gán cứng ngay từ hàm dựng như của bạn.

Từ đây rút ra được: phương thức getView() đặt tên tham số convertView có ý nghĩa cả. Nó tận dụng khung xem đã tạo trước đó để hiển thị lại các khung xem khác. Nhưng điều này gây khó khăn nếu không hiểu hết mục đích đó.

2 Likes

Để giải quyết được vấn đề của bạn trong trường hợp này thì cần theo tôn chỉ như sau:

  1. Các item trong list view có thể được dùng lại, do đó luôn sẵn sàng cho trường hợp item đã được dùng lại
  2. Không bao giờ update 1 view nếu không cần

Do đó mình gợi ý cho bạn 1 số gợi ý để fix như sau:

  1. Nên cache lại image đã hiển thị tại máy của người dùng đê lần load sau được nhanh hơn.
  2. Lưu lại position của view để khi load ảnh xong còn biết item đó có còn hiển thị đúng ảnh đã load hay chưa

Dưới đây là code mẫu của mình. Lưu ý có thể có bug do mình code bằng notepad. Bạn chỉ nên tham khảo và tự code lại theo ý tưởng

public class ImageLoader {
    private static final int LOADER_TAG = 999; // Số nào cũng được, không quan trọng lắm
    public ImageLoader() {}
        
    }

    public static void load(ImageView view, String url) {
        view.setTag(LOADER_TAG, url);
        new LoadingTask(view, url).execute();
    }

    private static class LoadingTask extends AsyncTask<Void, Void, Bitmap> {
        private final ImageView view;
        private final String url;

        public LoadingTask(ImageView view, String url) {
            this.view = view;
            this.url = url;
        }

        @Override
        protected Bitmap doInBackground(Void... strings) {
            URL url = null;
            try {
                url = new URL(this.url);
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
            HttpURLConnection connection = null;
            try {
                connection = (HttpURLConnection) url.openConnection();
            } catch (IOException e) {
                e.printStackTrace();
            }
            connection.setDoInput(true);
            try {
                connection.connect();
            } catch (IOException e) {
                e.printStackTrace();
            }
            InputStream input = null;
            try {
                input = connection.getInputStream();
            } catch (IOException e) {
                e.printStackTrace();
            }
            Bitmap myBitmap = BitmapFactory.decodeStream(input);
            return myBitmap;
        }

        @Override
        protected void onPostExecute(Bitmap result) {
            if(result != null) {
                String url = (String) this.view.getTag(LOADER_TAG);
                if (url == null || !this.url.equals(url)) { // URL thay đổi rồi
                    return;
                }
                this.view.setImageBitmap(result);
            }
        }
    }
}


@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ///.... Code của bạn
    if(viewHolder.imageViewNation != null){ 
        ImageLoader.load(viewHolder.imageViewNation, urlImage);
    }
}

Code này thiếu phần cache lại image đã được load về local/ Bạn thêm phần đó vào nữa nhé.
PS: tuyệt đối không được gọi method notifyDatasetChanged nếu không phải vì mục đích update lại view khi dữ liệu bị thay đổi nhé. Bị loop xử lí như chơi đấy

4 Likes
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?