четверг, 26 апреля 2018 г.

Создание корзины для мобильного интернет магазина

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

image

Вот такой у нас будет мини магазин, в котором у нас будет два экрана. Простенько и красивенько, дак еще и с дизайном :) 

За основу корзины я взял готовое решение корзины с github'a. Точно не помню с какого, так как там наплодилось куча подобных решений от разных мастеров копипасты. По этому укажу тот который нашел у себя в фейворитах и который больше всех похож на то что я использовал.

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

Seleable.java 
import java.math.BigDecimal;

public interface Saleable {
    BigDecimal getPrice();
    String getName();
}

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

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

CartItemsEntityModel.java
public class CartItemsEntityModel {
    private ProductEntityModel product;
    private int quantity;

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }

    public ProductEntityModel getProduct() {
        return product;
    }

    public void setProduct(ProductEntityModel product) {
        this.product = product;
    }
}

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

ProductEntityModel.java
import java.io.Serializable;
import java.math.BigDecimal;

import project.dajver.com.cartviewwithbadge.cart.helper.i.Saleable;

public class ProductEntityModel implements Saleable, Serializable {

    private static final long serialVersionUID = -4073256626483275668L;

    private Long id;
    private String name;
    private BigDecimal price;
    private String description;
    private String image;

    public ProductEntityModel() {
        super();
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) return false;
        if (!(o instanceof ProductEntityModel)) return false;

        return (this.id == ((ProductEntityModel) o).getId());
    }

    public int hashCode() {
        final int prime = 31;
        int hash = 1;
        hash = (int) (hash * prime + id);
        hash = hash * prime + (name == null ? 0 : name.hashCode());
        hash = hash * prime + (price == null ? 0 : price.hashCode());
        hash = hash * prime + (description == null ? 0 : description.hashCode());

        return hash;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public BigDecimal getPrice() {
        return price;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String pDescription) {
        this.description = pDescription;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String imageName) {
        this.image = imageName;
    }
}

Создали объект в котором будем хранить все детали покупки, сделали ее Serializable для доступости передачи этого объекта в Intent'aх или SharedPreferences и Saleable для того что бы хранить его в HashMap в дальнейшем, так как у нас все наши покупки будут находиться внутри этого объекта, который будет находиться внутри HashMap, и по ключу мы будем доставать его.

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

ProductNotFoundException.java
public class ProductNotFoundException extends RuntimeException {
    private static final long serialVersionUID = 43L;

    private static final String DEFAULT_MESSAGE = "Product is not found in the shopping cart.";

    public ProductNotFoundException() {
        super(DEFAULT_MESSAGE);
    }

    public ProductNotFoundException(String message) {
        super(message);
    }
}

Для создания сообщения в логах что продукт не найден.

QuantityOutOfRangeException.java
public class QuantityOutOfRangeException extends RuntimeException {
    private static final long serialVersionUID = 44L;

    private static final String DEFAULT_MESSAGE = "Quantity is out of range";

    public QuantityOutOfRangeException() {
        super(DEFAULT_MESSAGE);
    }

    public QuantityOutOfRangeException(String message) {
        super(message);
    }
}

Для отображения выход за пределы определенных границ.

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

CartEntity.java
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import project.dajver.com.cartviewwithbadge.cart.helper.exceptions.QuantityOutOfRangeException;
import project.dajver.com.cartviewwithbadge.cart.helper.exceptions.ProductNotFoundException;
import project.dajver.com.cartviewwithbadge.cart.helper.i.Saleable;

public class CartEntity implements Serializable {

    private static final long serialVersionUID = 42L;

    private Map<Saleable, Integer> cartItemMap = new HashMap<>();
    private BigDecimal totalPrice = BigDecimal.ZERO;
    private int totalQuantity = 0;

    public void add(final Saleable sellable, int quantity) {
        if (cartItemMap.containsKey(sellable)) {
            cartItemMap.put(sellable, cartItemMap.get(sellable) + quantity);
        } else {
            cartItemMap.put(sellable, quantity);
        }

        totalPrice = totalPrice.add(sellable.getPrice().multiply(BigDecimal.valueOf(quantity)));
        totalQuantity += quantity;
    }

    public void update(final Saleable sellable, int quantity) throws ProductNotFoundException, QuantityOutOfRangeException {
        if (!cartItemMap.containsKey(sellable)) throw new ProductNotFoundException();
        if (quantity < 0)
            throw new QuantityOutOfRangeException(quantity + " is not a valid quantity. It must be non-negative.");

        int productQuantity = cartItemMap.get(sellable);
        BigDecimal productPrice = sellable.getPrice().multiply(BigDecimal.valueOf(productQuantity));

        cartItemMap.put(sellable, quantity);

        totalQuantity = totalQuantity - productQuantity + quantity;
        totalPrice = totalPrice.subtract(productPrice).add(sellable.getPrice().multiply(BigDecimal.valueOf(quantity)));
    }

    public void remove(final Saleable sellable, int quantity) throws ProductNotFoundException, QuantityOutOfRangeException {
        if (!cartItemMap.containsKey(sellable)) throw new ProductNotFoundException();

        int productQuantity = cartItemMap.get(sellable);

        if (quantity < 0 || quantity > productQuantity)
            throw new QuantityOutOfRangeException(quantity + " is not a valid quantity. It must be non-negative and less than the current quantity of the product in the shopping cart.");

        if (productQuantity == quantity) {
            cartItemMap.remove(sellable);
        } else {
            cartItemMap.put(sellable, productQuantity - quantity);
        }

        totalPrice = totalPrice.subtract(sellable.getPrice().multiply(BigDecimal.valueOf(quantity)));
        totalQuantity -= quantity;
    }

    public void remove(final Saleable sellable) throws ProductNotFoundException {
        if (!cartItemMap.containsKey(sellable)) throw new ProductNotFoundException();

        int quantity = cartItemMap.get(sellable);
        cartItemMap.remove(sellable);
        totalPrice = totalPrice.subtract(sellable.getPrice().multiply(BigDecimal.valueOf(quantity)));
        totalQuantity -= quantity;
    }

    public void clear() {
        cartItemMap.clear();
        totalPrice = BigDecimal.ZERO;
        totalQuantity = 0;
    }

    public int getQuantity(final Saleable sellable) throws ProductNotFoundException {
        if (!cartItemMap.containsKey(sellable)) throw new ProductNotFoundException();
        return cartItemMap.get(sellable);
    }

    public BigDecimal getCost(final Saleable sellable) throws ProductNotFoundException {
        if (!cartItemMap.containsKey(sellable)) throw new ProductNotFoundException();
        return sellable.getPrice().multiply(BigDecimal.valueOf(cartItemMap.get(sellable)));
    }

    public BigDecimal getTotalPrice() {
        return totalPrice;
    }

    public int getTotalQuantity() {
        return totalQuantity;
    }

    public Set<Saleable> getProducts() {
        return cartItemMap.keySet();
    }

    public Map<Saleable, Integer> getItemWithQuantity() {
        Map<Saleable, Integer> cartItemMap = new HashMap<Saleable, Integer>();
        cartItemMap.putAll(this.cartItemMap);
        return cartItemMap;
    }

    @Override
    public String toString() {
        StringBuilder strBuilder = new StringBuilder();
        for (Map.Entry<Saleable, Integer> entry : cartItemMap.entrySet()) {
            strBuilder.append(String.format("Product: %s, Unit Price: %f, Quantity: %d%n", entry.getKey().getName(), entry.getKey().getPrice(), entry.getValue()));
        }
        strBuilder.append(String.format("Total Quantity: %d, Total Price: %f", totalQuantity, totalPrice));

        return strBuilder.toString();
    }
}

Здесь у нас все методы для работы с записью, обновлением и удалением продуктов из списка продуктов. Каждый метод отвечает за свою функцию, по этому объяснять я не вижу смысла что оно делает. У нас есть хешмеп — cartItemMap, в который мы записывает, из которого удаляем, и в котором обновляем по ключу — объекту наши данные.

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

CartHelper.java
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import project.dajver.com.cartviewwithbadge.cart.helper.i.Saleable;
import project.dajver.com.cartviewwithbadge.cart.helper.entity.CartEntity;
import project.dajver.com.cartviewwithbadge.cart.helper.entity.models.CartItemsEntityModel;
import project.dajver.com.cartviewwithbadge.cart.helper.entity.models.ProductEntityModel;

public class CartHelper {

    private static CartEntity cartEntity = new CartEntity();

    public static CartEntity getCart() {
        if (cartEntity == null) {
            cartEntity = new CartEntity();
        }

        return cartEntity;
    }

    public static List<CartItemsEntityModel> getCartItems() {
        List<CartItemsEntityModel> cartItems = new ArrayList<>();
        Map<Saleable, Integer> itemMap = getCart().getItemWithQuantity();

        for (Map.Entry<Saleable, Integer> entry : itemMap.entrySet()) {
            CartItemsEntityModel cartItem = new CartItemsEntityModel();
            cartItem.setProduct((ProductEntityModel) entry.getKey());
            cartItem.setQuantity(entry.getValue());
            cartItems.add(cartItem);
        }

        return cartItems;
    }
}

Тут у нас два метода, getCart() который возвращает объект CartEntity, который мы создали выше, что бы мы могли делать все возможные функции из этого класса. И getCartItems() который возвращает список товаров которые пользователь выбрал в корзину. Собственно и все. Теперь у нас корзина полностью готова, теперь будем учиться его использовать.

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

Начнем из далека, нам нужно подключить библиотеки которые мы будем использовать для работы, по традиции это как всегда будет ButterKnife, Picasso и RecyclerView.

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.5.2'

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

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

Значит у нас подключены библиотеки в dependencies, и мы еще подключили Java 8 для красивого сокращения кода, возможно какие-то функции понадобятся, в любом случае ретролямбда уже не нужна как раньше, теперь у нас это есть из коробки при помощи Java 8.

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

BaseActivity.java
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;

import butterknife.ButterKnife;
import project.dajver.com.cartviewwithbadge.cart.CartActivity;
import project.dajver.com.cartviewwithbadge.cart.helper.CartHelper;

public abstract class BaseActivity extends AppCompatActivity {

    private TextView textCartItemCount;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getViewId());
        ButterKnife.bind(this);
        onCreateView();
    }

    @Override
    public boolean onOptionsItemSelected(final MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                onBackPressed();
                return true;
            case R.id.cart:
                startActivity(new Intent(this, CartActivity.class));
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    private void setupBadge() {
        if (textCartItemCount != null) {
            if (CartHelper.getCartItems().size() == 0) {
                if (textCartItemCount.getVisibility() != View.GONE) {
                    textCartItemCount.setVisibility(View.GONE);
                }
            } else {
                textCartItemCount.setText(String.valueOf(Math.min(CartHelper.getCartItems().size(), 99)));
                if (textCartItemCount.getVisibility() != View.VISIBLE) {
                    textCartItemCount.setVisibility(View.VISIBLE);
                }
            }
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        final MenuItem menuItem = menu.findItem(R.id.cart);

        View actionView = MenuItemCompat.getActionView(menuItem);
        textCartItemCount = actionView.findViewById(R.id.cart_badge);

        setupBadge();
        actionView.setOnClickListener(v -> onOptionsItemSelected(menuItem));

        return true;
    }

    public abstract int getViewId();
    public abstract void onCreateView();
}

Вот собственно о том что я говорил. В onCreate() мы подключаем баттернайф, указали абстрактный метод для прописывания леяута, и вызвали в конце onCreateView() который у нас будет проходить вся логика при создании активити. 
onOptionsItemSelected() — выполняет стандартную функцию по для отслеживания клика пользователя на меню.
setupBadge() — у нас создает бадж на иконке если у нас есть товары в корзине, и если нету то прячет его.
onCreateOptionsMenu() — создает кастомную иконку с баджем поверх ее. По клику на нее мы вызываем onOptionsItemSelected().

Ну и два абстрактных метода для переопределения функций getViewId() и onCreateView().

menu_main.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/cart"
        android:orderInCategory="100"
        android:title="@string/cart"
        app:actionLayout="@layout/view_custom_action_cart"
        android:icon="@mipmap/cart"
        app:showAsAction="always"/>

</menu>

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

view_custom_action_cart.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    style="?attr/actionButtonStyle"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:clipToPadding="false"
    android:focusable="true">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:src="@mipmap/cart"/>

    <TextView
        android:id="@+id/cart_badge"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:layout_gravity="right|end|top"
        android:layout_marginEnd="-5dp"
        android:layout_marginRight="-5dp"
        android:layout_marginTop="3dp"
        android:background="@drawable/badge_background"
        android:gravity="center"
        android:padding="3dp"
        android:textColor="@color/black"
        android:text="0"
        android:textSize="10sp"/>

</FrameLayout>

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

badge_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">

    <solid android:color="@color/orangeText"/>
    <stroke android:color="@color/black" android:width="1dp"/>

</shape>

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

color.xml
    <color name="orangeText">#ffbb38</color>
    <color name="black">#000</color>
    <color name="white">#fff</color>

По красоте все, у нас будет стильно :) Далее создаем базовый фрагмент, он простой до ужаса.

BaseFragment.java
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import butterknife.ButterKnife;

public abstract class BaseFragment extends Fragment {

    public Activity context;

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        context = getActivity();
        onViewCreated(view);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(getViewId(), container, false);
        ButterKnife.bind(this, rootView);
        setHasOptionsMenu(true);
        return rootView;
    }

    public abstract int getViewId();
    public abstract void onViewCreated(View view);
}

Тут тоже самое что мы делали в BaseActivity только без подключения меню. setHasOptionsMenu(true) — вызываем для того что бы меню можно было вызывать из фрагмента, тут нам оно не нужно, но на всякий случай я вставил и забыл его удалить. :)

ProductActivity.java
import project.dajver.com.cartviewwithbadge.BaseActivity;
import project.dajver.com.cartviewwithbadge.R;

public class ProductActivity extends BaseActivity {

    @Override
    public int getViewId() {
        return R.layout.activity_product;
    }

    @Override
    public void onCreateView() { }
}

Пустой как моя жизнь. Тут нам ничего не надо, но вообще тут можно что-то добавить что-то что душе угодно, любой функционал. Вся логика будет в ProductFragment. XML будет выглядеть так:

activity_product.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"
    xmlns:tools="http://schemas.android.com/tools">

    <fragment
        android:id="@+id/products"
        android:name="project.dajver.com.cartviewwithbadge.product.ProductFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:layout="@layout/fragment_product" />

</LinearLayout>

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

ProductFragment.java
import android.content.Intent;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

import java.math.BigDecimal;

import butterknife.BindView;
import project.dajver.com.cartviewwithbadge.BaseFragment;
import project.dajver.com.cartviewwithbadge.R;
import project.dajver.com.cartviewwithbadge.cart.CartActivity;
import project.dajver.com.cartviewwithbadge.cart.helper.CartHelper;
import project.dajver.com.cartviewwithbadge.cart.helper.entity.CartEntity;
import project.dajver.com.cartviewwithbadge.cart.helper.entity.models.ProductEntityModel;
import project.dajver.com.cartviewwithbadge.etc.ProductsHelper;
import project.dajver.com.cartviewwithbadge.product.adapter.ProductRecyclerAdapter;
import project.dajver.com.cartviewwithbadge.product.adapter.models.ProductModel;

public class ProductFragment extends BaseFragment implements ProductRecyclerAdapter.OnItemClickListener {

    @BindView(R.id.recyclerView)
    RecyclerView recyclerView;

    @Override
    public int getViewId() {
        return R.layout.fragment_product;
    }

    @Override
    public void onViewCreated(View view) {
        ProductRecyclerAdapter productRecyclerAdapter = new ProductRecyclerAdapter(context, ProductsHelper.getProductsList());
        productRecyclerAdapter.setOnItemClickListener(this);
        recyclerView.setLayoutManager(new GridLayoutManager(context, 2));
        recyclerView.setAdapter(productRecyclerAdapter);
    }

    @Override
    public void onItemClick(ProductModel productModel) {
        ProductEntityModel product = new ProductEntityModel();
        product.setId(productModel.getId());
        product.setName(productModel.getTitle());
        product.setDescription(productModel.getDescription());
        product.setPrice(BigDecimal.valueOf(productModel.getPrice()));
        product.setImage(productModel.getImage());

        CartEntity cart = CartHelper.getCart();
        cart.add(product, 1);

        Intent intent = new Intent(context, CartActivity.class);
        startActivity(intent);

        getActivity().invalidateOptionsMenu();
    }
}

В onViewCreated() — создаем адаптер который мы еще не создали, но он у нас должен быть. Сетим его в ресайклер вью и показываем по красоте. Да, а еще у нас есть еще класс который содержит в себе продукты которые у нас отображает адаптер, ну вообще в идеале конечно у нас должно подтягиваться из серверной части, но тут у нас будет все локально.

fragment_product.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.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

У нас тут только список который будет отображаться со списком товаров.

ProductModel.java
public class ProductModel {

    private Long id;
    private Integer price;
    private String image;
    private String title;
    private String description;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

Это объект который у нас описывает как будет выглядить продукт — айди, имя, описание, цена и картинка.

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

import project.dajver.com.cartviewwithbadge.product.adapter.models.ProductModel;

public class ProductsHelper {
    public static List<ProductModel> getProductsList() {
        List<ProductModel> productModels = new ArrayList<>();

        ProductModel model = new ProductModel();
        model.setId(5678l);
        model.setTitle("Honor 6A 2");
        model.setDescription("16Gb Grey");
        model.setImage("https://github.com/dajver/CartWithBadgeExample/blob/master/images/honor.jpg?raw=true");
        model.setPrice(599);
        productModels.add(model);

        model = new ProductModel();
        model.setId(5672l);
        model.setTitle("Meizu M5s");
        model.setDescription("32Gb Silver");
        model.setImage("https://github.com/dajver/CartWithBadgeExample/blob/master/images/meizu.jpg?raw=true");
        model.setPrice(899);
        productModels.add(model);

        model = new ProductModel();
        model.setId(5673l);
        model.setTitle("Apple iPhone SE");
        model.setDescription("32Gb Space Gray");
        model.setImage("https://github.com/dajver/CartWithBadgeExample/blob/master/images/iphone.jpg?raw=true");
        model.setPrice(1199);
        productModels.add(model);

        model = new ProductModel();
        model.setId(5674l);
        model.setTitle("Chuwi Hi10 Pro");
        model.setImage("https://github.com/dajver/CartWithBadgeExample/blob/master/images/chuwi.jpg?raw=true");
        model.setPrice(2199);
        productModels.add(model);

        model = new ProductModel();
        model.setId(5675l);
        model.setTitle("Fermi S7-plus");
        model.setDescription("10000mAh gray)");
        model.setImage("https://github.com/dajver/CartWithBadgeExample/blob/master/images/batary.jpg?raw=true");
        model.setPrice(259);
        productModels.add(model);

        return productModels;
    }
}

В общем то, тут объяснять нечего просто создали несколько продуктов для красоты, продолжим рассматривать ProductFragment. 
onItemClick() — у нас по клику на кнопку бай переходит в корзину, в которой у нас уже будет отображаться список покупок для отправки. Каждый клик добавляем 1 товар в корзину, и обновляем меню что бы отображался бадж.

И давайте создадим адаптер. В нем мы просто отображаем наш список товаров.

ProductRecyclerAdapter.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.Button;
import android.widget.ImageView;
import android.widget.TextView;

import com.squareup.picasso.Picasso;

import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import project.dajver.com.cartviewwithbadge.R;
import project.dajver.com.cartviewwithbadge.product.adapter.models.ProductModel;

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

    private List<ProductModel> catalogModels;
    private OnItemClickListener onItemClickListener;
    private Context context;

    public ProductRecyclerAdapter(Context context, List<ProductModel> catalogModels) {
        this.context = context;
        this.catalogModels = catalogModels;
    }

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

    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
        ReceiveViewHolder viewHolder = (ReceiveViewHolder) holder;
        viewHolder.title.setText(catalogModels.get(position).getTitle());
        viewHolder.description.setText(catalogModels.get(position).getDescription());
        viewHolder.price.setText(String.format(context.getString(R.string.dollars_format), catalogModels.get(position).getPrice()));
        Picasso.with(context).load(catalogModels.get(position).getImage()).into(viewHolder.image);
    }

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

    public class ReceiveViewHolder extends RecyclerView.ViewHolder {

        @BindView(R.id.title)
        TextView title;
        @BindView(R.id.description)
        TextView description;
        @BindView(R.id.image)
        ImageView image;
        @BindView(R.id.price)
        TextView price;
        @BindView(R.id.buyButton)
        Button buyButton;

        public ReceiveViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            buyButton.setOnClickListener(view -> {
                onItemClickListener.onItemClick(catalogModels.get(getAdapterPosition()));
            });
        }
    }

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

    public interface OnItemClickListener {
        void onItemClick(ProductModel productModel);
    }
}

Собственно все тривиально, те кто писали адаптеры кастомные — не должно составить труда понять что тут происходит. В onCreateViewHolder() — проинициализировали леяут. В onBindViewHolder() — забиндили все данные что мы передали в catalogModels. Ну и в ReceiveViewHolder забиндили все вьюхи что нам нужны и устанавливаем на что у нас будет клик.

XML у нас будет крутой. У нас тут будет и картинка, и описание, и цена. И это все будет выглядит круто и красивенько.

item_product.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:layout_marginLeft="10dp"
    android:layout_marginTop="10dp"
    android:background="@color/white"
    android:foreground="?android:selectableItemBackground"
    android:gravity="center_vertical|center_horizontal"
    android:orientation="vertical"
    android:padding="20dp">

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

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center_vertical|center_horizontal"
        android:text="Samsung Galaxy J7"
        android:textColor="@color/black"
        android:textSize="18sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_vertical|center_horizontal"
        android:maxLines="4"
        android:text="Plus Duos 64 GB  Orchid Gray" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:orientation="horizontal">

        <Button
            android:id="@+id/buyButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:backgroundTint="@color/orangeText"
            android:text="Buy" />

        <TextView
            android:id="@+id/price"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="right|center_vertical"
            android:text="$1999"
            android:textColor="@android:color/black"
            android:textSize="20sp"
            android:textStyle="bold" />
    </LinearLayout>

</LinearLayout>

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

strings.xml
<resources>
    <string name="app_name">CartViewWithBadge</string>

    <string name="cart">Cart</string>
    <string name="products">Products</string>
    <string name="buy">Buy</string>

    <string name="cart.success.message" formatted="false">You bought %s products from your cart. With total price $%s</string>
    <string name="dollars.format" formatted="false">$%s</string>
</resources>

Собственно это нам понадобится в дальнейшем, по этому пусть будет тут.

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

CartActivity.java
import project.dajver.com.cartviewwithbadge.BaseActivity;
import project.dajver.com.cartviewwithbadge.R;

public class CartActivity extends BaseActivity {

    @Override
    public int getViewId() {
        return R.layout.activity_cart;
    }

    @Override
    public void onCreateView() {
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
    }
}

onCreateView() — отображаем кнопку назад. Собственно и все.

activity_cart.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"
    xmlns:tools="http://schemas.android.com/tools">

    <fragment
        android:id="@+id/cart"
        android:name="project.dajver.com.cartviewwithbadge.cart.CartFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:layout="@layout/fragment_cart" />

</LinearLayout>

Тоже самое что и в продукте, по этому ничего нового :) Просто цепляем фрагмент к активити, для разделения логики и жизненного цикла.

CartFragment.java
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Toast;

import butterknife.BindView;
import butterknife.OnClick;
import project.dajver.com.cartviewwithbadge.BaseFragment;
import project.dajver.com.cartviewwithbadge.R;
import project.dajver.com.cartviewwithbadge.cart.adapter.CartRecyclerAdapter;
import project.dajver.com.cartviewwithbadge.cart.helper.CartHelper;
import project.dajver.com.cartviewwithbadge.cart.helper.entity.models.CartItemsEntityModel;

public class CartFragment extends BaseFragment implements CartRecyclerAdapter.OnItemClickListener {

    @BindView(R.id.recyclerView)
    RecyclerView recyclerView;

    private CartRecyclerAdapter productRecyclerAdapter;

    @Override
    public int getViewId() {
        return R.layout.fragment_cart;
    }

    @Override
    public void onViewCreated(View view) {
        onUpdateList();
    }

    @Override
    public void onItemClick(CartItemsEntityModel cartItemsEntityModel) {
        // open details of product
    }

    @Override
    public void onItemPlusClicked(int position, CartItemsEntityModel cartItemsEntityModel) {
        int quantity = cartItemsEntityModel.getQuantity();
        CartItemsEntityModel cartModel = new CartItemsEntityModel();
        cartModel.setProduct(cartItemsEntityModel.getProduct());
        quantity++;
        cartModel.setQuantity(quantity);
        productRecyclerAdapter.updateItem(position, cartModel);
    }

    @Override
    public void onItemMinusClicked(int position, CartItemsEntityModel cartItemsEntityModel) {
        int quantity = cartItemsEntityModel.getQuantity();
        CartItemsEntityModel cartModel = new CartItemsEntityModel();
        cartModel.setProduct(cartItemsEntityModel.getProduct());
        quantity--;
        cartModel.setQuantity(quantity);
        productRecyclerAdapter.updateItem(position, cartModel);
    }

    @Override
    public void onUpdateList() {
        productRecyclerAdapter = new CartRecyclerAdapter(context, CartHelper.getCartItems());
        productRecyclerAdapter.setOnItemClickListener(this);
        recyclerView.setLayoutManager(new LinearLayoutManager(context));
        recyclerView.setAdapter(productRecyclerAdapter);
    }

    @OnClick(R.id.buyButton)
    public void onBuyClick() {
        Toast.makeText(context, String.format(getString(R.string.cart_success_message), CartHelper.getCart().getTotalQuantity(), CartHelper.getCart().getTotalPrice()), Toast.LENGTH_LONG).show();
        CartHelper.getCart().clear();
        getActivity().finish();
    }
}

Тут у нас вообще прям работа тетаническая выполнена. В onViewCreated() у нас вызывается onUpdateList() который у нас создается адаптер и сетим данные в него. Нам нужен метод onUpdateList() для того что бы обновлять данные в списке когда мы изменяем количество по клику на кнопку "+" или "-". Ну и передаем в адаптер CartHelper.getCartItems() который у нас хранит все наши товары.
onItemMinusClicked() — у нас по клику уменьшает количество айтемов в адаптере, и в самой корзине.
onItemPlusClicked() — соответственно увеличивает количество.
onBuyClick() — по клику на кнопку Buy, мы отображаем тост с количеством купленного товара, и общую сумму на которую мы купили. Дальше стираем для вида что купили из списка все, и возвращаемся в список товаров.

fragment_cart.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:orientation="vertical">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="0.1">

    </android.support.v7.widget.RecyclerView>

    <Button
        android:id="@+id/buyButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:backgroundTint="@color/orangeText"
        android:onClick="onBuyClick"
        android:text="@string/buy" />

</LinearLayout>

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

CartRecyclerAdapter.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.Button;
import android.widget.ImageView;
import android.widget.TextView;

import com.squareup.picasso.Picasso;

import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import project.dajver.com.cartviewwithbadge.R;
import project.dajver.com.cartviewwithbadge.cart.helper.CartHelper;
import project.dajver.com.cartviewwithbadge.cart.helper.entity.models.CartItemsEntityModel;

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

    private List<CartItemsEntityModel> productEntityModel;
    private OnItemClickListener onItemClickListener;
    private Context context;

    public CartRecyclerAdapter(Context context, List<CartItemsEntityModel> productEntityModel) {
        this.context = context;
        this.productEntityModel = productEntityModel;
    }

    public void updateItem(int position, CartItemsEntityModel cartItemsEntityModel) {
        if(cartItemsEntityModel.getQuantity() > 0) {
            productEntityModel.set(position, cartItemsEntityModel);
            CartHelper.getCart().update(cartItemsEntityModel.getProduct(), cartItemsEntityModel.getQuantity());
        } else {
            CartHelper.getCart().remove(productEntityModel.get(position).getProduct());
            onItemClickListener.onUpdateList();
        }
        notifyDataSetChanged();
    }

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

    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
        ReceiveViewHolder viewHolder = (ReceiveViewHolder) holder;
        viewHolder.name.setText(productEntityModel.get(position).getProduct().getName());
        viewHolder.description.setText(productEntityModel.get(position).getProduct().getDescription());
        viewHolder.price.setText(String.format(context.getString(R.string.dollars_format), productEntityModel.get(position).getProduct().getPrice()));
        viewHolder.quantity.setText(String.valueOf(productEntityModel.get(position).getQuantity()));
        Picasso.with(context).load(productEntityModel.get(position).getProduct().getImage()).into(viewHolder.image);
    }

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

    public class ReceiveViewHolder extends RecyclerView.ViewHolder {

        @BindView(R.id.name)
        TextView name;
        @BindView(R.id.description)
        TextView description;
        @BindView(R.id.image)
        ImageView image;
        @BindView(R.id.price)
        TextView price;
        @BindView(R.id.quantity)
        TextView quantity;
        @BindView(R.id.plus)
        Button plus;
        @BindView(R.id.minus)
        Button minus;

        public ReceiveViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            itemView.setOnClickListener(view -> {
                onItemClickListener.onItemClick(productEntityModel.get(getAdapterPosition()));
            });
            minus.setOnClickListener(view -> {
                onItemClickListener.onItemMinusClicked(getAdapterPosition(), productEntityModel.get(getAdapterPosition()));
            });
            plus.setOnClickListener(view -> {
                onItemClickListener.onItemPlusClicked(getAdapterPosition(), productEntityModel.get(getAdapterPosition()));
            });
        }
    }

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

    public interface OnItemClickListener {
        void onItemClick(CartItemsEntityModel cartItemsEntityModel);
        void onItemPlusClicked(int position, CartItemsEntityModel cartItemsEntityModel);
        void onItemMinusClicked(int position, CartItemsEntityModel cartItemsEntityModel);
        void onUpdateList();
    }
}

Значит то что у нас в конструкторе мы сетим список который у нас в корзине я думаю понятно. Дальше в методе updateItem() мы или увеличиваем количество определенного товара или уменьшаем. Если уменьшаем до 0 то просто стираем товар из списка. Ну а дальше по старинке onCreateViewHolder() — сетит леяут. onBindViewHolder() биндит то что у нас в списке в вьюхи.

item_cart.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="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="10dp"
    android:layout_marginTop="10dp"
    android:background="@color/white"
    android:foreground="?android:selectableItemBackground"
    android:gravity="center_vertical|center_horizontal"
    android:orientation="horizontal"
    android:padding="20dp">

    <ImageView
        android:id="@+id/image"
        android:layout_width="100dp"
        android:layout_height="100dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="20dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="TextView"
            android:textColor="@color/black"
            android:textSize="18sp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/description"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="10dp"
            android:layout_weight="0.1"
            android:text="TextView" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <Button
                android:id="@+id/minus"
                android:layout_width="25dp"
                android:layout_height="25dp"
                android:background="@drawable/button_cart_round_background_gradient"
                android:text="-"
                android:textColor="@color/white"
                android:textSize="18sp" />

            <TextView
                android:id="@+id/quantity"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginRight="20dp"
                android:text="1"
                android:textColor="@color/black"
                android:textSize="18sp"
                android:textStyle="bold" />

            <Button
                android:id="@+id/plus"
                android:layout_width="25dp"
                android:layout_height="25dp"
                android:background="@drawable/button_cart_round_background_gradient"
                android:text="+"
                android:textColor="@color/white"
                android:textSize="18sp" />

            <TextView
                android:id="@+id/price"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="right"
                android:text="TextView"
                android:textColor="@color/black"
                android:textSize="18sp"
                android:textStyle="bold" />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

Ну тут у нас в общем все тоже самое что было и в продукте, только немного изменили до вида корзины, добавили кнопки "+" и "-". 

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

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="project.dajver.com.cartviewwithbadge">

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity
            android:name=".product.ProductActivity"
            android:label="@string/products">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".cart.CartActivity"
            android:label="@string/cart" />
    </application>

</manifest>

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

Исходники:
GitHub

Комментариев нет:

Отправить комментарий