Поиск по этому блогу

вторник, 22 мая 2018 г.

GalleryView своими руками

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

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


По сути это будет вью в которую мы будем скармливать List в котором у нас будут ссылки на картинки, и дальше при помощи магии оно будет это отображать.

Начнем мы с того что подключим все нужные библиотеки которые нам нужны для отображения картинок, списков, и цеплянию view.

app/build.gradle
android {
    ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'

    implementation 'com.squareup.picasso:picasso:2.71828'

    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

    implementation 'com.android.support:recyclerview-v7:27.1.1'
}

Примерно вот так будет выглядеть наш градл файл, все собственно было выше описано.
support:appcompat — нужен для того что бы у нас подтянулись все нужные классы относящиеся к andorid sdk, фрагменты, активити и т.д.
picasso:picasso — нужен для отображения картинок.
jakewharton:butterknife — нужен для упрощения подключать вьюхи, вместо findViewById мы просто будем писать @BindView(Id), и все.
support:recyclerview — нужен для отображения списков.

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

ImageThumbsRecyclerAdapter.java
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import com.squareup.picasso.Picasso;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import project.dajver.com.galleryview.R;

public class ImageThumbsRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

    private List<View> selectedViews = new ArrayList<>();
    private List<View> allViews = new ArrayList<>();
    private List<String> productImagesModels;
    private OnItemClickListener onItemClickListener;
    private Context context;

    public ImageThumbsRecyclerAdapter(Context context, List<String> productImagesModels) {
        this.context = context;
        this.productImagesModels = productImagesModels;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_horizontal_image_thumbs, parent, false);
        return new ReceiveViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
        ReceiveViewHolder viewHolder = (ReceiveViewHolder) holder;
        Picasso.get().load(productImagesModels.get(position)).into(viewHolder.image);
        allViews.add(viewHolder.itemView);
    }

    @Override
    public int getItemCount() {
        return productImagesModels.size();
    }

    public void setCurrentItemActive(int position) {
        for(int i = 0; i < selectedViews.size(); i++) {
            selectedViews.remove(i).setSelected(false);
        }
        selectedViews.add(allViews.get(position));
        allViews.get(position).setSelected(true);
    }

    public class ReceiveViewHolder extends RecyclerView.ViewHolder {

        @BindView(R.id.image)
        ImageView image;

        public ReceiveViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            itemView.setOnClickListener(view -> {
                for(int i = 0; i < selectedViews.size(); i++) {
                    selectedViews.remove(i).setSelected(false);
                }
                selectedViews.add(itemView);
                itemView.setSelected(true);
                onItemClickListener.onItemClick(productImagesModels.get(getAdapterPosition()), getAdapterPosition());
            });
        }
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    public interface OnItemClickListener {
        void onItemClick(String imagePath, int position);
    }
}

В контструкторе мы принимаем List со всем списком картинок для отображения. так же у нас есть в шапке адаптера два списка — selectedViews и allViews. Они нам нужны для сохранения кликнутых вьюх и удаления остальных отмеченных, об этом чуть ниже.

В onBindViewHolder() у нас идет заполнение всех вьюх которые мы имеем в списке, и так же заполнение массива allViews, для того что бы потом из него убирать или добавлять в него кликутые вьюхи. 

Метод setCurrentItemActive() которые делает кликнутый айтем выделеным, и убирает выделение со всех остальных айтемов в списке. 

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

item_horizontal_image_thumbs.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="75dp"
    android:layout_height="75dp"
    android:layout_marginLeft="5dp"
    android:background="@drawable/image_thumbs_selector"
    android:foreground="?android:selectableItemBackground"
    android:gravity="center_vertical|center_horizontal"
    android:orientation="vertical"
    android:padding="10dp">

    <ImageView
        android:id="@+id/image"
        android:layout_width="75dp"
        android:layout_height="75dp"
        app:srcCompat="@mipmap/ic_launcher" />

</LinearLayout>

Наш айтем будет состоять всего лишь из одного ImageView в котором мы будем отображать наши превьюшки.

Так же у нас в этом xml подключен селектор выделения айтема image_thumbs_selector, он нам нужен что бы в нужный момент выделять при помощи setSelected() метода кликнутый айтем.

image_thumbs_selector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true"
        android:drawable="@drawable/selected_image_thumb"/>
    <item android:state_selected="false"
        android:drawable="@android:color/transparent"/>
</selector>

А так же в нем у нас вызывается selected_image_thumb который рисует вокруг вьюхи тонкую обводку.

selected_image_thumb.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <stroke android:width="1dp" android:color="@color/colorPrimary"/>
</shape>

Делаем тонкую обводку в 1 дпи вокруг айтема, цветом colorPrimary который у нас прописан в colors.xml.

Далее нам нужно создать слайдер для того что бы можно было при помощи свайпов влево и вправо так же листать наши картинки и внизу в нашем слайдере с превью они так же переключались. 

ImageSliderPagerAdapter.java
import android.content.Context;
import android.support.v4.view.PagerAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import com.squareup.picasso.Picasso;

import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import project.dajver.com.galleryview.R;

public class ImageSliderPagerAdapter extends PagerAdapter {

    private List<String> images;
    private LayoutInflater inflater;
    private Context context;

    public ImageSliderPagerAdapter(Context context, List<String> images) {
        this.context = context;
        this.images=images;
        inflater = LayoutInflater.from(context);
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View) object);
    }

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

    @Override
    public Object instantiateItem(ViewGroup viewGroup, int position) {
        View view = inflater.inflate(R.layout.item_image_slider, viewGroup, false);
        ViewHolder viewHolder = new ViewHolder(view);
        Picasso.get().load(images.get(position)).into(viewHolder.image);
        viewGroup.addView(view, 0);
        return view;
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view.equals(object);
    }

    public class ViewHolder {

        @BindView(R.id.image)
        ImageView image;

        public ViewHolder(View view) {
            ButterKnife.bind(this, view);
        }
    }
}

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

item_image_slider.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <ImageView
        android:id="@+id/image"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_gravity="center"
        android:adjustViewBounds="true"
        android:scaleType="fitCenter" />
</FrameLayout>

Опять же, одна единственная ImageView в которой будем отображать картинку в нашем view pager.

Дальше нам нужно объеденить эти два адаптера в одной вьюхе что бы они взаимодействовали друг с другом и были одным единым в этой вьюхе. Для начала давайте посмотрим как у нас это будет выглядеть в xml.

view_image_slider_with_preview.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="400dp"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentTop="true"
        android:layout_weight="0.1">

    </android.support.v4.view.ViewPager>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/imagesRecyclerView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp" />

</LinearLayout>

Как видно из кода, у нас тут всего два элемента, это ViewPager который мы будем использовать для отображения большой картинки и RecyclerView для отображения превью картинок.

ImageSliderWithPreviewView.java
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.widget.LinearLayout;

import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import project.dajver.com.galleryview.R;
import project.dajver.com.galleryview.adapters.ImageSliderPagerAdapter;
import project.dajver.com.galleryview.adapters.ImageThumbsRecyclerAdapter;

public class ImageSliderWithPreviewView extends LinearLayout implements ImageThumbsRecyclerAdapter.OnItemClickListener,
        ViewPager.OnPageChangeListener {

    @BindView(R.id.pager)
    ViewPager viewPager;
    @BindView(R.id.imagesRecyclerView)
    RecyclerView imageThumbsRecyclerView;

    private Context context;
    private ImageThumbsRecyclerAdapter thumbsRecyclerView;

    public ImageSliderWithPreviewView(Context context) {
        super(context);
        init(context);
    }

    public ImageSliderWithPreviewView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ImageSliderWithPreviewView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        this.context = context;
        inflate(context, R.layout.view_image_slider_with_preview, this);
        ButterKnife.bind(this);
    }

    public void setImageList(List<String> imageList) {
        if(imageList != null && imageList.size() > 0) {
            viewPager.setAdapter(new ImageSliderPagerAdapter(context, imageList));
            viewPager.addOnPageChangeListener(this);
            thumbsRecyclerView = new ImageThumbsRecyclerAdapter(context, imageList);
            thumbsRecyclerView.setOnItemClickListener(this);
            imageThumbsRecyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
            imageThumbsRecyclerView.setAdapter(thumbsRecyclerView);
        }
    }

    @Override
    public void onItemClick(String catalogModel, int position) {
        viewPager.setCurrentItem(position);
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        thumbsRecyclerView.setCurrentItemActive(position);
    }

    @Override
    public void onPageSelected(int position) { }

    @Override
    public void onPageScrollStateChanged(int state) {  }
}

И тут у нас уже идет объединение всего выше написанного кода в один организм. В init() мы проинициализировали леяут, butter knife и контекст. 

Дальше у нас идет метод setImageList() в который мы передаем List который дальше пойдет в наши адаптеры для отображения картинок. Так же в этом методе мы задаем View Pager'у OnPageChangeListener который будет в зависимости от свайпа отображать нужную вьюху и передавать в адаптер превью которое нужно выделить. Ну и OnItemClickListener для отслеживания клика по превью что бы открыть определенную картинку в View pager'e.

В onPageScrolled() мы задаем какую картинку в превью выделить с помощью метода setCurrentItemActive() про который я писал выше.

Ну и теперь осталось только добавить эту вьюху на activity_main и скормить ей список картинок в MainActivity.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dp">

    <project.dajver.com.galleryview.views.ImageSliderWithPreviewView
        android:id="@+id/imageSliderView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

Как видно из кода, достаточно просто добавить ее на экран, и все, больше никаких параметров не нужно. Только нужно задать id ей, и все. И дальше в MainActivity скормить List.

MainActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import project.dajver.com.galleryview.views.ImageSliderWithPreviewView;

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.imageSliderView)
    ImageSliderWithPreviewView imageSliderWithPreviewView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        List<String> productImagesModels = new ArrayList<>();
        productImagesModels.add("https://github.com/dajver/GalleryView/blob/master/imgs/image1.jpg?raw=true");
        productImagesModels.add("https://github.com/dajver/GalleryView/blob/master/imgs/image2.jpg?raw=true");
        productImagesModels.add("https://github.com/dajver/GalleryView/blob/master/imgs/image3.jpg?raw=true");
        productImagesModels.add("https://github.com/dajver/GalleryView/blob/master/imgs/image4.jpg?raw=true");
        productImagesModels.add("https://github.com/dajver/GalleryView/blob/master/imgs/image5.jpg?raw=true");
        productImagesModels.add("https://github.com/dajver/GalleryView/blob/master/imgs/image6.jpg?raw=true");

        imageSliderWithPreviewView.setImageList(productImagesModels);
    }
}

И собственно все что требуется от вас. Дальше оно там будет само подставлять картинки, рисовать вьюху с адаптером и отрабатывать свайпы и клики. Ну не прекрасно ли это?

Так же нужно не забыть добавить в манифест пермишен для работы с интернетом, а то картинки не подтянутся.

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />

Вот собственно и все что требовалось от нас для того что бы сделать такую вьюху.

Исходники:
GitHub