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

среда, 24 мая 2017 г.

Создаем CustomView из LinearLayout

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

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

Для того что бы нам сделать такое. Нам нужно создать вьюху которая будет выглядеть так мы хотим что бы оно выглядело. Я захотел сделать ее такой:
image

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

XML данной вьюхи будет выглядеть так:

view_top_tabs.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="10dp"
    android:paddingRight="10dp">

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

        <Button
            android:text="@string/opinion.last.week"
            android:background="@drawable/selected_rounded_button"
            android:textColor="@color/white"
            android:layout_width="match_parent"
            android:layout_weight="0.1"
            android:layout_height="40dp"
            android:id="@+id/lastWeek" />

        <Button
            android:text="@string/opinion.last.month"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:id="@+id/lastMonth"
            android:background="@drawable/unselected_tranding_rounded_button"
            android:textColor="@color/colorPrimary"
            android:layout_weight="0.1"
            android:layout_marginLeft="10dp" />

        <Button
            android:text="@string/opinion.all.time"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:id="@+id/allTime"
            android:background="@drawable/unselected_tranding_rounded_button"
            android:textColor="@color/colorPrimary"
            android:layout_weight="0.1"
            android:layout_marginLeft="10dp" />
    </LinearLayout>

</LinearLayout>

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

Так же тут не хватает нам нескольких цветов и пары drawable файлов, в нашем случае цвета такие:

color.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#FF4081</color>

    <color name="white">#fff</color>
</resources>

Дальше наши drawable файлы, первый у нас скругленный но пустой внутри для отображения активного таба.

selected_rounded_button.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" android:padding="10dp">
    <solid android:color="@color/colorPrimaryDark"/>
    <corners
        android:bottomRightRadius="30dp"
        android:bottomLeftRadius="30dp"
        android:topLeftRadius="30dp"
        android:topRightRadius="30dp"/>
</shape>

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

unselected_tranding_rounded_button.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" android:padding="10dp">
    <solid android:color="@color/white"/>
    <corners
        android:bottomRightRadius="30dp"
        android:bottomLeftRadius="30dp"
        android:topLeftRadius="30dp"
        android:topRightRadius="30dp"/>
    <stroke android:width="2dp"
        android:color="@color/colorPrimary"/>
</shape>

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

TopTabsView.java
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;

import com.project.customviewproject.R;

import butterknife.BindColor;
import butterknife.BindView;
import butterknife.ButterKnife;

/**
 * Created by gleb on 5/24/17.
 */

public class TopTabsView extends LinearLayout implements View.OnClickListener {

    //Константы для определения какие у нас есть кнопки на вьюхе
    public static final int LAST_WEEK = 0;
    public static final int LAST_MONTH = 1;
    public static final int ALL_TIME = 2;

    //биндим вьюхи с помощью butter knife
    @BindView(R.id.lastWeek)
    Button lastWeek;
    @BindView(R.id.lastMonth)
    Button lastMonth;
    @BindView(R.id.allTime)
    Button allTime;

    //биндим цвета с помощью той же либы
    @BindColor(R.color.white)
    int whiteColor;
    @BindColor(R.color.colorPrimary)
    int blueColor;

    //Это наш лисенер для нажатия на кнопки, мы его создадим позже
    private OnTopTabsViewClickListener onTopTabsViewClickListener;
    //текущий таб
    private int currentTab;

    // конструкторы для сетапа вьюх на нем.
    public TopTabsView(Context context) {
        super(context);
        init(context);
    }

    public TopTabsView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
        setParams(context, attrs);
    }

    public TopTabsView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
        setParams(context, attrs);
    }

    // этот метод для для установки параметров которые мы задали в файле attrs.xml
    private void setParams(Context context, AttributeSet attrs) {
        //находим наш параметер в файле attrs
        TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.TopTabsComponent);
        //берем его что бы дальше передать по умолчанию
        int tabId = arr.getInt(R.styleable.TopTabsComponent_tabId, LAST_WEEK);
        //передаем по умолчанию что бы была выбранна какая-то кнопка по умолчанию
        setActiveTab(tabId);
        arr.recycle();
    }

    // что то типа onCreate для вьюх, прописываем что используем как и зачем
    private void init(Context context) {
        //сетапим XML которую будем использовать для кастомного таба
        inflate(context, R.layout.view_top_tabs, this);
        //ставим ориентацию, не сильно важный метод, но маст хев в некоторых случаях
        setOrientation(VERTICAL);
        //это у нас для того что бы использовать @BindView и @BindColor
        ButterKnife.bind(this);
        setUp();
    }

    // задали листенеры для onClick
    private void setUp() {
        lastWeek.setOnClickListener(this);
        lastMonth.setOnClickListener(this);
        allTime.setOnClickListener(this);
    }

    //метод который выделает кликнутую вьюху или дефолтную
    public void setActiveTab(int tabId) {
        currentTab = tabId;
        switch (tabId) {
            case LAST_WEEK:
                lastWeek.setBackgroundResource(R.drawable.selected_rounded_button);
                lastWeek.setTextColor(whiteColor);
                lastMonth.setBackgroundResource(R.drawable.unselected_tranding_rounded_button);
                lastMonth.setTextColor(blueColor);
                allTime.setBackgroundResource(R.drawable.unselected_tranding_rounded_button);
                allTime.setTextColor(blueColor);
                break;
            case LAST_MONTH:
                lastWeek.setBackgroundResource(R.drawable.unselected_tranding_rounded_button);
                lastWeek.setTextColor(blueColor);
                lastMonth.setBackgroundResource(R.drawable.selected_rounded_button);
                lastMonth.setTextColor(whiteColor);
                allTime.setBackgroundResource(R.drawable.unselected_tranding_rounded_button);
                allTime.setTextColor(blueColor);
                break;
            case ALL_TIME:
                lastWeek.setBackgroundResource(R.drawable.unselected_tranding_rounded_button);
                lastWeek.setTextColor(blueColor);
                lastMonth.setBackgroundResource(R.drawable.unselected_tranding_rounded_button);
                lastMonth.setTextColor(blueColor);
                allTime.setBackgroundResource(R.drawable.selected_rounded_button);
                allTime.setTextColor(whiteColor);
                break;
        }
    }

    public int getCurrentTab() {
        return currentTab;
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.lastWeek:
                //по клику делаем вьюху активной
                setActiveTab(LAST_WEEK);
                //отправляем колбек в активити что бы поменять фрагмент на экране
                onTopTabsViewClickListener.onTopTabViewClick(LAST_WEEK);
                break;
            case R.id.lastMonth:
                setActiveTab(LAST_MONTH);
                onTopTabsViewClickListener.onTopTabViewClick(LAST_MONTH);
                break;
            case R.id.allTime:
                setActiveTab(ALL_TIME);
                onTopTabsViewClickListener.onTopTabViewClick(ALL_TIME);
                break;
        }
    }

    //сеттер для колбека
    public void setOnTopTabsViewClickListener(OnTopTabsViewClickListener onTopSubTabsViewClickListener) {
        this.onTopTabsViewClickListener = onTopSubTabsViewClickListener;
    }

    //сам интерфейс
    public interface OnTopTabsViewClickListener {
        //таб айди для определния по какой вьюхе кликнули
        int onTopTabViewClick(int tabId);
    }
}

Так же нам не хватает файла который описывает константы из attrs файла. 

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TopTabsComponent">
        <attr name="tabId" format="integer">
            <enum name="lastWeek" value="0" />
            <enum name="lastMonth" value="1" />
            <enum name="allTime" value="2" />
        </attr>
    </declare-styleable>
</resources>

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

activity_main.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="match_parent"
    android:orientation="vertical">

    <com.project.customviewproject.view.TopTabsView
        android:id="@+id/topBarView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        app:tabId="lastWeek"></com.project.customviewproject.view.TopTabsView>

    <FrameLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="10dp">

    </FrameLayout>

</LinearLayout>

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

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

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

import com.project.customviewproject.ui.AllTimeFragment;
import com.project.customviewproject.ui.LastMonthFragment;
import com.project.customviewproject.ui.LastWeekFragment;
import com.project.customviewproject.view.TopTabsView;

import butterknife.BindView;
import butterknife.ButterKnife;

import static com.project.customviewproject.view.TopTabsView.ALL_TIME;
import static com.project.customviewproject.view.TopTabsView.LAST_MONTH;
import static com.project.customviewproject.view.TopTabsView.LAST_WEEK;

public class MainActivity extends AppCompatActivity implements TopTabsView.OnTopTabsViewClickListener {

    // инициализируем таб бар
    @BindView(R.id.topBarView)
    TopTabsView topTabsView;

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

        // инициализируем лисенер для клика на табы
        topTabsView.setOnTopTabsViewClickListener(this);
        // задаем текущий таб по умолчанию что бы показывало тот фрагмент который у нас по дефолту установлен в XML
        onTopTabViewClick(topTabsView.getCurrentTab());
    }

    //дальше по клику врубаем какой хотим фрагмент
    @Override
    public int onTopTabViewClick(int tabId) {
        Fragment fragment = null;
        switch (tabId) {
            case LAST_WEEK:
                fragment = new LastWeekFragment();
                break;
            case LAST_MONTH:
                fragment = new LastMonthFragment();
                break;
            case ALL_TIME:
                fragment = new AllTimeFragment();
                break;
        }
        setupFragment(fragment);
        return LAST_WEEK;
    }

    //метод который реализует открытие фрагмента
    private void setupFragment(Fragment fragment) {
        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.container, fragment)
                .commit();
    }
}

Так же нам нужно создать три фрагмента с разными надписями, ну или там с разными данными, списками или чем-то еще… Я создал три разные вьюхи просто с разными текствью. 

fragment_last_week

<?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|center_horizontal"
    android:orientation="vertical"
    android:background="@color/white">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/opinion.last.week"
        android:textSize="24sp" />
</LinearLayout>

LastWeekFragment.java
public class LastWeekFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_last_week, container, false);
        ButterKnife.bind(this, view);
        return view;
    }
}


fragment_last_month

<?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|center_horizontal"
    android:orientation="vertical"
    android:background="@color/white">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/opinion.last.month"
        android:textSize="24sp" />
</LinearLayout>

LastMonthFragment.java
public class LastMonthFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_last_month, container, false);
        ButterKnife.bind(this, view);
        return view;
    }
}

fragment_all_time

<?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|center_horizontal"
    android:orientation="vertical"
    android:background="@color/white">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/opinion.all.time"
        android:textSize="24sp" />
</LinearLayout>

AllTimeFragment.java
public class AllTimeFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_all_time, container, false);
        ButterKnife.bind(this, view);
        return view;
    }
}

Результатом у нас будет активити с тремя табами, по клику на которые мы будем переходить по фрагментам.
imageimage

Исходники:

GitHub