Отображение изображений в вашем пользовательском интерфейсе

Этот урок объединяет все предыдущие уроки, показывая вам, как загрузить несколько растровых изображений в ViewPager и GridView компоненты с помощью фонового потока и используя кэш растровых изображений, обрабатывая проблемы параллелизма и изменения конфигурации.

Загрузка растровых изображений в реализацию ViewPager

Шаблон перемещающихся представлений является отличным способом для перемещения по подробный представлениям галереи изображений. Вы можете реализовать эту модель с помощью ViewPager компонент при поддержке PagerAdapter. Тем не менее, больше подходит один из подклассов адаптера FragmentStatePagerAdapter , который автоматически уничтожает и сохраняет состояние Fragments в ViewPager , как только они исчезают с экрана, сохраняя низким объем используемой памяти.

Примечание: Если у вас есть небольшое количество изображений и уверены, все они поместились в пределах памяти приложения, то использование обычного PagerAdapter или FragmentPagerAdapter может быть более целесообразным.

Вот реализация ViewPager с ImageView дочерними элементами. Основная деятельность удерживает ViewPager и адаптер:

public class ImageDetailActivity extends FragmentActivity {
    public static final String EXTRA_IMAGE = "extra_image";

    private ImagePagerAdapter mAdapter;
    private ViewPager mPager;

    // A static dataset to back the ViewPager adapter
    public final static Integer[] imageResIds = new Integer[] {
            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.image_detail_pager); // Contains just a ViewPager

        mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
        mPager = (ViewPager) findViewById(R.id.pager);
        mPager.setAdapter(mAdapter);
    }

    public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
        private final int mSize;

        public ImagePagerAdapter(FragmentManager fm, int size) {
            super(fm);
            mSize = size;
        }

        @Override
        public int getCount() {
            return mSize;
        }

        @Override
        public Fragment getItem(int position) {
            return ImageDetailFragment.newInstance(position);
        }
    }
}

Вот реализация детального Fragment , который содержит ImageView родительские представления. Это может показаться совершенно разумным подходом, но можете ли вы увидеть недостатки этой реализации? Как это можно улучшить?

public class ImageDetailFragment extends Fragment {
    private static final String IMAGE_DATA_EXTRA = "resId";
    private int mImageNum;
    private ImageView mImageView;

    static ImageDetailFragment newInstance(int imageNum) {
        final ImageDetailFragment f = new ImageDetailFragment();
        final Bundle args = new Bundle();
        args.putInt(IMAGE_DATA_EXTRA, imageNum);
        f.setArguments(args);
        return f;
    }

    // Empty constructor, required as per Fragment docs
    public ImageDetailFragment() {}

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // image_detail_fragment.xml contains just an ImageView
        final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
        mImageView = (ImageView) v.findViewById(R.id.imageView);
        return v;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        final int resId = ImageDetailActivity.imageResIds[mImageNum];
        mImageView.setImageResource(resId); // Load image into ImageView
    }
}

Надеюсь вы заметили проблему: изображения вычитываются из ресурсов в потоке пользовательского интерфейса, что может привести к зависанию приложения и оно будет принудительно закрыто. Используйте AsyncTask , как описано в уроке Обработка растровых изображений вне потока пользовательского интерфейса , это просто переместит загрузку и обработку изображений в фоновый поток:

public class ImageDetailActivity extends FragmentActivity {
    ...

    public void loadBitmap(int resId, ImageView imageView) {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }

    ... // include BitmapWorkerTask class
}

public class ImageDetailFragment extends Fragment {
    ...

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (ImageDetailActivity.class.isInstance(getActivity())) {
            final int resId = ImageDetailActivity.imageResIds[mImageNum];
            // Call out to ImageDetailActivity to load the bitmap in a background thread
            ((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
        }
    }
}

Любая дополнительная обработка (например, изменение размера или извлечение изображений из сети) может происходить в BitmapWorkerTask не затрагивая реакцию главного пользовательского интерфейса. Если фоновый поток делает больше, чем просто загрузку изображения непосредственно с диска, то также может быть полезно добавить кэш памяти и/или дисковый кэш, как описано в занятии Кэширование растровых изображений. Вот дополнительные модификации для кэш-памяти:

public class ImageDetailActivity extends FragmentActivity {
    ...
    private LruCache<String, Bitmap> mMemoryCache;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        // initialize LruCache as per Use a Memory Cache section
    }

    public void loadBitmap(int resId, ImageView imageView) {
        final String imageKey = String.valueOf(resId);

        final Bitmap bitmap = mMemoryCache.get(imageKey);
        if (bitmap != null) {
            mImageView.setImageBitmap(bitmap);
        } else {
            mImageView.setImageResource(R.drawable.image_placeholder);
            BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
            task.execute(resId);
        }
    }

    ... // include updated BitmapWorkerTask from Use a Memory Cache section
}

Сопоставление всех этих частей вместе дает отзывчивую ViewPager реализацию с минимальной задержкой при загрузке изображений, и способность делать обработку изображений в фоновом потоке по мере необходимости.

Загрузка растровых изображений в реализацию GridView

Строительный блок Сетка полезен для отображения наборов изображений и может быть реализован с помощью GridView компонента, в котором много изображений может быть на экране в любой момент времени и много других, готовых появиться, если пользователь прокручивает вверх или вниз. При реализации этого типа управления, вы должны убедиться, что пользовательский интерфейс остается плавным, использование памяти остается под контролем и параллелизм правильно обрабатывается (благодаря способности GridView перерабатывать свои дочерние представления).

Для начала, здесь стандартная GridView реализация с ImageView дочерними представлениями, помещенными внутрь Fragment. Опять же, это может показаться совершенно разумный подходом, но как бы сделать ещё лучше?

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
    private ImageAdapter mAdapter;

    // A static dataset to back the GridView adapter
    public final static Integer[] imageResIds = new Integer[] {
            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};

    // Empty constructor as per Fragment docs
    public ImageGridFragment() {}

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new ImageAdapter(getActivity());
    }

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
        final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
        mGridView.setAdapter(mAdapter);
        mGridView.setOnItemClickListener(this);
        return v;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
        i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
        startActivity(i);
    }

    private class ImageAdapter extends BaseAdapter {
        private final Context mContext;

        public ImageAdapter(Context context) {
            super();
            mContext = context;
        }

        @Override
        public int getCount() {
            return imageResIds.length;
        }

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

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

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ImageView imageView;
            if (convertView == null) { // if it's not recycled, initialize some attributes
                imageView = new ImageView(mContext);
                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
                imageView.setLayoutParams(new GridView.LayoutParams(
                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
            } else {
                imageView = (ImageView) convertView;
            }
            imageView.setImageResource(imageResIds[position]); // Load image into ImageView
            return imageView;
        }
    }
}

Еще раз, проблема с этой реализации в том, что изображение задается в потоке пользовательского интерфейса. Хотя это может работать для небольших, простых изображений (благодаря загрузке системных ресурсов и кэшированию), но если необходима любая дополнительная обработка, то ваш пользовательский интерфейс со скрипом остановится.

Асинхронная обработка и методы кэширования из предыдущего раздела могут быть так же реализованы здесь. Тем не менее, вы также должны опасаться проблем параллелизма, так как GridView перерабатывает свои дочерние представления. Чтобы решить эти проблемы, используйте методы, описанные в уроке Обработка растровых изображений вне потока пользовательского интерфейса . Вот обновленное решение:

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
    ...

    private class ImageAdapter extends BaseAdapter {
        ...

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ...
            loadBitmap(imageResIds[position], imageView)
            return imageView;
        }
    }

    public void loadBitmap(int resId, ImageView imageView) {
        if (cancelPotentialWork(resId, imageView)) {
            final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
            final AsyncDrawable asyncDrawable =
                    new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
            imageView.setImageDrawable(asyncDrawable);
            task.execute(resId);
        }
    }

    static class AsyncDrawable extends BitmapDrawable {
        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

        public AsyncDrawable(Resources res, Bitmap bitmap,
                BitmapWorkerTask bitmapWorkerTask) {
            super(res, bitmap);
            bitmapWorkerTaskReference =
                new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
        }

        public BitmapWorkerTask getBitmapWorkerTask() {
            return bitmapWorkerTaskReference.get();
        }
    }

    public static boolean cancelPotentialWork(int data, ImageView imageView) {
        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

        if (bitmapWorkerTask != null) {
            final int bitmapData = bitmapWorkerTask.data;
            if (bitmapData != data) {
                // Cancel previous task
                bitmapWorkerTask.cancel(true);
            } else {
                // The same work is already in progress
                return false;
            }
        }
        // No task associated with the ImageView, or an existing task was cancelled
        return true;
    }

    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
       if (imageView != null) {
           final Drawable drawable = imageView.getDrawable();
           if (drawable instanceof AsyncDrawable) {
               final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
               return asyncDrawable.getBitmapWorkerTask();
           }
        }
        return null;
    }

    ... // include updated BitmapWorkerTask class

Примечание: Тот же самый код может быть легко адаптирован для работы с ListView.

Эта реализация обеспечивает гибкость в том, как изображения обрабатываются и загружаются, не препятствуя плавности пользовательского интерфейса. В фоновом режиме вы можете загрузить изображения из сети, или изменять размер больших фотографий с цифровой камеры, и изображения отображаются как только задача заканчивает обработку.

Полный пример этого и других принципов, обсуждаемых в этом уроке, ищите к прилагаемом примере приложения.