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

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

Кастомная CarouselView в домашних условиях

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

Для реализации карусели я использовал готовую библиотеку CarouselView но немного ее изменив.

Что бы вы понимали что такое карусель, вот пример того что реализует библиотека которую я использовал для реализации своей
image

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




В общем начнем делать нашу карусель. Добавляем несколько библиотек в build.gradle.

app/build.gradle
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'

    implementation 'com.gtomato.android.library:carouselview:2.0.1'

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

    implementation 'com.squareup.picasso:picasso:2.5.2'
}

Тут мы добавили ButterKnife для подгрузки вьюх, Picasso для загрузки картинок и CarouselView для карусели. Дальше нам нужно написать собственный класс «трансформатор» который будет создавать 3Д эффект.

FlatMarryGoRoundSyndTransformer.java
import android.view.View;

import com.gtomato.android.ui.manager.CarouselLayoutManager;
import com.gtomato.android.ui.widget.CarouselView;

public class FlatMarryGoRoundSyndTransformer implements CarouselView.ViewTransformer {

    private int mNumPies = 3;
    private double mPieRad = Math.PI * 2.0 / mNumPies;

    private double mHorizontalViewPort = 0.55;
    private double mViewPerspective = -0.6;
    private double mFarScale = 0.55;

    @Override
    public void onAttach(CarouselLayoutManager layoutManager) {
        layoutManager.setDrawOrder(CarouselView.DrawOrder.CenterFront);
    }

    @Override
    public void transform(View view, float position) {
        int parentWidth = ((View) view.getParent()).getMeasuredWidth();
        int parentHeight = ((View) view.getParent()).getMeasuredHeight();

        double rotateRad = Math.PI * 1.5 + position * mPieRad;
        double a = parentWidth * mHorizontalViewPort / 2.0;
        double b = parentHeight * mViewPerspective / 3;

        double x = a * Math.cos(rotateRad);
        double y = b * (1 - Math.sin(rotateRad));

        double maxY = 2 * b;

        double scale = Math.max(0, (mFarScale - 1) * (y - maxY) / (0 - maxY) + 1);

        y -= maxY;

        view.setTranslationX((float) x);
        view.setTranslationY((float) y);
        view.setScaleX((float) scale);
        view.setScaleY((float) scale);
    }
}

Тут очень сложная математика происходит, я не уверен что я сам понял что сделал, но в целом постараюсь описать логику. Все настройки для работы карусели у нас находятся в самом верху класса в переменных mNumPies, mPieRad, mHorizontalViewPort, mViewPerspective и mFarScale.
mNumPies — количество айтемов на экране.
mPieRad — с его помощью мы расчитываем количество айтемов.
mHorizontalViewPort — горизонтальная перспектива для отображения айтемов.
mViewPerspective — вертикальная перспектива с которой будут отображаться айтемы.
mFarScale — как далеко они будут находиться друг от друга.

В самом верху метода transform() мы получаем размеры вьюхи что бы поместить на нее нашу горзинтально скролящийся RecyclerView. Дальше по формуле элипса мы раскладываем перспективу

x(t) = a cos t
y(t) = b sin t
where t ∈ [0, 2π]

Далее у нас размер будет зависеть от mViewPerspective, то есть если мы поставим не -0.6, а на пример 1 — скейл увеличится и будет так что картинки будут по бокам будут больше чем центральная. Дальше переставляем центральную позицию айтема так что бы он был y 

∈ [-maxY/2, maxY/2]
то есть по центру главный и по бокам все остальные элементы списка. Дальше задаем все эти данные для перемещения по x и y, и для размера на экране. По этому классу все.

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

CarouselRecyclerAdapter.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.carouselview.R;

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

    private List<String> carouselList = new ArrayList<>();
    private Context context;

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

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View rowView = LayoutInflater.from(context).inflate(R.layout.item_carousel, null);
        return new ViewHolder(rowView);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        ViewHolder viewHolder = (ViewHolder) holder;
        Picasso.with(context).load(carouselList.get(position)).into(viewHolder.image);
    }

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

    public class ViewHolder extends RecyclerView.ViewHolder{

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

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

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

item_carousel.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="wrap_content"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/promImage"
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:layout_gravity="center_horizontal" />

</LinearLayout>

Добавляем на наш айтем ImageView, размером 100 по ширине и 150 по высоте, что бы был как постер. И дальше собираем все это до кучи в нашем MainActivity. XML для 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:gravity="center_vertical">

    <com.gtomato.android.ui.widget.CarouselView
        android:id="@+id/carousel"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_marginBottom="5dp" />

</LinearLayout>

По сути просто разместили карусель вью на экране для использовани в дальнейших извращениях.

MainActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Toast;

import com.gtomato.android.ui.widget.CarouselView;

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

import butterknife.BindView;
import butterknife.ButterKnife;
import project.dajver.com.carouselview.adapter.CarouselRecyclerAdapter;
import project.dajver.com.carouselview.view.FlatMarryGoRoundSyndTransformer;

public class MainActivity extends AppCompatActivity implements CarouselView.OnItemClickListener {

    @BindView(R.id.carousel)
    CarouselView carouselView;

    private List<String> carouselList;

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

        carouselList = new ArrayList<>();
        carouselList.add("https://s-media-cache-ak0.pinimg.com/originals/f2/d2/eb/f2d2eb69fb80e312d310c32786bf8182.png");
        carouselList.add("https://vignette.wikia.nocookie.net/marveldatabase/images/1/1a/The_Avengers_%28film%29_poster_011.jpg");
        carouselList.add("https://i.pinimg.com/originals/13/35/b1/1335b128be40578eec379be437f02de7.jpg");
        carouselList.add("https://i.pinimg.com/originals/6d/2b/b3/6d2bb3023ada5ea320f54bb4fc3fe50a.jpg");
        carouselList.add("http://moviemarker.co.uk/wp-content/uploads/2012/03/Thor-Film-Poster.jpg");
        carouselList.add("http://www.cbc.ca/strombo/content/images/a-cool-look-at-rejected-posters-for-classic-films-feature2.jpg");
        carouselList.add("http://mediablogs.keshacademy.com/cmacleannanwatkinsas/files/2016/09/free-movie-film-poster-harry-potter-phoenix-2iwng7l.jpg");

        CarouselRecyclerAdapter carouselRecyclerAdapter = new CarouselRecyclerAdapter(this, carouselList);
        carouselView.setTransformer(new FlatMarryGoRoundSyndTransformer());
        carouselView.setAdapter(carouselRecyclerAdapter);
        carouselView.setOnItemClickListener(this);
    }

    @Override
    public void onItemClick(RecyclerView.Adapter adapter, View view, int position, int adapterPosition) {
        Toast.makeText(this, carouselList.get(position), Toast.LENGTH_LONG).show();
    }
}

В onCreate() мы задали список картинок, дальше передали этот список в адаптер, потом задали этот адаптер в карусель вью и задали наш трансформер FlatMarryGoRoundSyndTransformer для карусели, который мы только вот написали. Ну и присвоили onClick метод для карусели. 

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

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

И вот так вот мы смогли сделать красивенькую карусель для отображения картинок в приложении. Возможно это будет кому-то полезно.

Исходники:
GitHub