Введение
Для того, чтобы разработать хорошее приложение, недостаточно лишь наполнить его функционалом. Важно также, чтобы интерфейс приложения был удобен и понятен пользователю, а также выглядел презентабельно. Приложение, в котором пользователь не знает, куда нужно нажимать, вряд ли будет пользоваться популярностью.
Отличным инструментом для создания продуманного интерфейса в Android являются свайпы (Swipe). С их помощью всего лишь одним-двумя жестами можно выполнять самые разные действия с объектами: перемещать их по экрану, удалять, изменять и много другое. Например, можно с помощью свайпа выводить на экран приложения такие элементы интерфейса, как боковое меню.
В этой статье мы рассмотрим, как добавить свайп для виджета CardView, расположенного внутри RecyclerView, который будет выводить с правой стороны карточки кнопку. Для примера делать мы это будем в одном из наших приложений Менеджер паролей от Wi-Fi сетей.
Задачей здесь является сделать кнопку, которая будет перемещать сети из списка активных в Архив (или удалять, если сеть уже находится в Архиве).
Создание RecyclerView
Поскольку список сетей может быть довольно большим, в приложении используется виджет RecyclerView. Чтобы использовать его в своём проекте, нужно для начала добавить зависимость. Для этого в файле build.gradle модуля приложения в dependencies нужно добавить следующую строку:
dependencies {
...
compile "com.android.support:recyclerview-v7:27.0.2"
...
}
После этого нужно разместить виджет на разметке активности. Пример этого можно увидеть ниже:
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
/>
Наше приложение отображает список сетей, сохранённых пользователем. Чтобы RecyclerView смог выводить этот список, для начала нужен класс, который будет хранить данные о сетях. Для этих целей используется класс WifiInfo, содержащий следующий код:
public class WifiInfo implements Serializable {
public WifiInfo(String SSID, String password, String type, boolean hidden, Date date,
String state) {
this.SSID = SSID;
this.password = password;
this.type = type;
this.hidden = hidden;
this.date = date;
this.state = state;
}
public String ID;
public String SSID;
public String password;
public String type;
public boolean hidden;
public String state;
public Date date;
public WifiInfo() {
}
@Exclude public Map<String, Object> toMap() {
HashMap<String, Object> result = new HashMap<>();
result.put("ID", ID);
result.put("SSID", SSID);
result.put("password", password);
result.put("type", type);
result.put("hidden", hidden);
result.put("date", date);
result.put("state", state);
return result;
}
@Override public boolean equals(Object obj) {
if (obj instanceof WifiInfo) {
WifiInfo temp = (WifiInfo) obj;
if (this.SSID.equals(temp.SSID) && this.password.equals(temp.password) && this.type.equals(
temp.type) && this.hidden == temp.hidden) {
return true;
}
}
return false;
}
@Override public int hashCode() {
return (this.SSID.hashCode() + this.password.hashCode() + this.type.hashCode());
}
}
Следующим этапом является создание адаптера. Класс RecyclerView.Adapter крайне важен здесь, поскольку благодаря ему данные, хранящиеся в классе WifiInfo, будут выводиться в итоговый список. В классе нужно переопределить несколько методов, таких как:
- getItemCount() — возвращает количество элементов, которое мы хотим отобразить;
- onCreateViewHolder() — создает экземпляр класса RecyclerView.ViewHolder и создает разметку. Он вызывается только тогда, когда RecyclerView требуется добавить в список новый объект.
- onBindViewHolder() — привязывает данные к разметке ViewHolder. Он вызывается тогда, когда RecyclerView заполняет объект данными.
Ниже вы можете увидеть код адаптера, реализующего заполнение списка данными по сетям.
public class PasswordAdapter extends RecyclerView.Adapter<PasswordAdapter.PasswordViewHolder> {
public interface dbCallBack {
void removeFromDb(WifiInfo wi);
void addToDbCB(WifiInfo wi);
void moveToHistory(WifiInfo wi);
}
private List<WifiInfo> passwordList, passwordListCopy;
private Context context;
private dbCallBack db;
private LinearLayout desertPlaceholder;
private boolean cardHide = true;
private Drawable copy, share;
private int collapsedHeight;
PasswordAdapter(Context context, dbCallBack db, LinearLayout desertPlaceholder) {
this.context = context;
this.passwordList = new ArrayList<>();//new ArrayList<>(passwordList);
passwordListCopy = new ArrayList<>();
this.db = db;
this.desertPlaceholder = desertPlaceholder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
copy = ContextCompat.getDrawable(context, R.drawable.ic_content_copy_black_24px);
share = ContextCompat.getDrawable(context, R.drawable.ic_share_black_24px);
} else {
copy =
VectorDrawableCompat.create(context.getResources(), R.drawable.ic_content_copy_black_24px,
context.getTheme());
share = VectorDrawableCompat.create(context.getResources(), R.drawable.ic_share_black_24px,
context.getTheme());
}
}
void filter(String text) {
passwordList.clear();
if (text.isEmpty()) {
passwordList.addAll(passwordListCopy);
} else {
text = text.toLowerCase();
for (WifiInfo item : passwordListCopy) {
if (item.SSID.toLowerCase().contains(text)) {
passwordList.add(item);
}
}
}
notifyDataSetChanged();
}
@Override public int getItemCount() {
desertPlaceholder.setVisibility(passwordList.size() > 0 ? View.GONE : View.VISIBLE);
return passwordList.size();
}
@Override public void onBindViewHolder(final PasswordViewHolder passVH, int i) {
final WifiInfo wi = passwordList.get(i);
passVH.vSSID.setText(wi.SSID);
passVH.vPassword.setText(wi.password);
String pattern = ((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.MEDIUM,
Locale.getDefault())).toPattern();
DateFormat dateFormat = new SimpleDateFormat(pattern, Locale.getDefault());
if (passVH.swipeLayout.isOpened()) {
passVH.swipeLayout.close(false);
}
if (!cardHide) {
passVH.cardMore.setVisibility(View.GONE);
passVH.delete.setLayoutParams(
new FrameLayout.LayoutParams(passVH.delete.getWidth(), collapsedHeight));
cardHide = true;
checkArrow(passVH.cardArrow);
}
passVH.ivShare.setImageDrawable(share);
passVH.ivCopy.setImageDrawable(copy);
passVH.vDate.setText(dateFormat.format(wi.date));
if (wi.hidden) {
passVH.vHide.setVisibility(View.VISIBLE);
passVH.vDot.setVisibility(View.VISIBLE);
}
if (wi.state.equals("active")) {
Drawable drawable;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
drawable =
VectorDrawableCompat.create(context.getResources(), R.drawable.ic_archive_black_24px,
context.getTheme());
} else {
drawable = ContextCompat.getDrawable(context, R.drawable.ic_archive_black_24px);
}
passVH.ivDelete.setImageDrawable(drawable);
passVH.vDelete.setText(context.getString(R.string.to_archive));
passVH.delete.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
db.moveToHistory(wi);
passVH.swipeLayout.close(true);
}
});
} else {
Drawable drawable;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
drawable =
VectorDrawableCompat.create(context.getResources(), R.drawable.ic_delete_black_24px,
context.getTheme());
} else {
drawable = ContextCompat.getDrawable(context, R.drawable.ic_delete_black_24px);
}
passVH.ivDelete.setImageDrawable(drawable);
passVH.vDelete.setText(context.getString(R.string.delete));
passVH.delete.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
App.selectContent("card", "удалить");
removeItem(wi);
db.removeFromDb(wi);
Snackbar.make(passVH.itemView, wi.SSID + " " + context.getString(R.string.removed),
Snackbar.LENGTH_LONG).setAction(R.string.cancel, new View.OnClickListener() {
@Override public void onClick(View v) {
addItem(wi);
db.addToDbCB(wi);
}
}).show();
}
});
}
passVH.vCopy.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
App.selectContent("card", "копировать");
Tools.CopyToClipboard(context, wi.password);
Toast.makeText(context, R.string.Copy_value, Toast.LENGTH_LONG).show();
}
});
passVH.vConnect.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
App.selectContent("card", "подключить сеть");
WifiManager wifiManager =
(WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
if (wifiManager == null) {
return;
}
final Activity activity = (Activity) context;
activity.runOnUiThread(new Runnable() {
@Override public void run() {
Toast.makeText(context, R.string.wifi_changing_network, Toast.LENGTH_SHORT).show();
}
});
WifiConfigManager wcf = new WifiConfigManager(wifiManager);
if (wcf.getStatus().toString().equals("PENDING")) wcf.execute(wi);
}
});
passVH.vShare.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
PopupMenu popupMenu = new PopupMenu(context, v);
MenuInflater menuInflater = popupMenu.getMenuInflater();
PopUpMenuEventHandle popUpMenuEventHandle = new PopUpMenuEventHandle(context, wi);
popupMenu.setOnMenuItemClickListener(popUpMenuEventHandle);
menuInflater.inflate(R.menu.share_popup, popupMenu.getMenu());
popupMenu.show();
}
});
passVH.cardMain.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
if (cardHide) {
passVH.cardMore.setVisibility(View.VISIBLE);
collapsedHeight = passVH.cardMain.getHeight();
passVH.delete.setLayoutParams(new FrameLayout.LayoutParams(passVH.delete.getWidth(),
ViewGroup.LayoutParams.MATCH_PARENT));
cardHide = false;
} else {
passVH.cardMore.setVisibility(View.GONE);
passVH.delete.setLayoutParams(
new FrameLayout.LayoutParams(passVH.delete.getWidth(), collapsedHeight));
cardHide = true;
}
checkArrow(passVH.cardArrow);
}
});
}
@Override public PasswordViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View itemView = LayoutInflater.
from(viewGroup.getContext()).
inflate(R.layout.card_network, viewGroup, false);
return new PasswordViewHolder(itemView);
}
private void checkArrow(ImageView imageView) {
if (cardHide) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
imageView.setImageDrawable(VectorDrawableCompat.create(context.getResources(),
R.drawable.ic_keyboard_arrow_down_black_24dp, context.getTheme()));
} else {
imageView.setImageDrawable(
ContextCompat.getDrawable(context, R.drawable.ic_keyboard_arrow_down_black_24dp));
}
} else {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
imageView.setImageDrawable(VectorDrawableCompat.create(context.getResources(),
R.drawable.ic_keyboard_arrow_up_black_24dp, context.getTheme()));
} else {
imageView.setImageDrawable(
ContextCompat.getDrawable(context, R.drawable.ic_keyboard_arrow_up_black_24dp));
}
}
}
public List<WifiInfo> getList() {
return passwordList;
}
public void addAll(List<WifiInfo> list) {
passwordList.addAll(list);
passwordListCopy.addAll(list);
notifyDataSetChanged();
}
public void clearAll() {
passwordList.clear();
passwordListCopy.clear();
notifyDataSetChanged();
}
WifiInfo removeItem(int position) {
WifiInfo model = passwordList.remove(position);
passwordListCopy.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, passwordList.size());
return model;
}
void removeItem(final WifiInfo model) {
int pos = passwordList.indexOf(model);
if (pos > -1 && pos < passwordList.size()) {
passwordList.remove(model);
passwordListCopy.remove(model);
notifyItemRemoved(pos);
}
}
void addItem(WifiInfo model) {
if (!passwordListCopy.contains(model)) {
passwordList.add(model);
passwordListCopy.add(model);
Tools.sort(passwordList);
Tools.sort(passwordListCopy);
notifyItemInserted(passwordList.indexOf(model));
}
}
public void moveItem(int fromPosition, int toPosition) {
final WifiInfo model = passwordList.remove(fromPosition);
passwordList.add(toPosition, model);
notifyItemMoved(fromPosition, toPosition);
}
public WifiInfo getItem(int position) {
return passwordList.get(position);
}
static class PasswordViewHolder extends RecyclerView.ViewHolder {
SwipeRevealLayout swipeLayout;
LinearLayout cardMain, cardMore, vCopy, vShare;
TextView vSSID, vPassword, vHide, vDate, vConnect, vDot, vDelete;
FrameLayout delete;
ImageView cardArrow, ivCopy, ivShare, ivDelete;
PasswordViewHolder(View v) {
super(v);
vDelete = v.findViewById(R.id.tv_delete);
ivDelete = v.findViewById(R.id.iv_delete);
delete = v.findViewById(R.id.delete_layout);
swipeLayout = v.findViewById(R.id.swipe_layout);
cardMain = v.findViewById(R.id.card_main);
cardMore = v.findViewById(R.id.card_more);
vSSID = v.findViewById(R.id.SSID);
vPassword = v.findViewById(R.id.password);
vHide = v.findViewById(R.id.tv_hide);
vCopy = v.findViewById(R.id.CopyButton);
vConnect = v.findViewById(R.id.ConnectButton);
vShare = v.findViewById(R.id.ShareButton);
vDate = v.findViewById(R.id.date);
cardArrow = v.findViewById(R.id.card_arrow);
vDot = v.findViewById(R.id.tv_dot);
ivCopy = v.findViewById(R.id.iv_card_copy);
ivShare = v.findViewById(R.id.iv_card_share);
vPassword.setCompoundDrawablesWithIntrinsicBounds(
AppCompatResources.getDrawable(vPassword.getContext(), R.drawable.ic_key_black_24dp),
null, null, null);
}
}
}
Здесь же, как можно увидеть, создаётся класс ViewHolder, который описывает разметку элемента и метаданные о его месте в RecyclerView. Реализация адаптера должна использовать ViewHolder в качестве подкласса, поскольку в них кешируются затратные по ресурсам операции findViewById().
Теперь нужно создать разметку, которая будет подгружаться во ViewHolder. Она представляет собой два виджета FrameLayout, помещённых внутри кастомного SwipeRevealLayout, о котором будет сказано позднее. Код разметки вы можете увидеть ниже.
<?xml version="1.0" encoding="utf-8"?>
<com.rusdelphi.wifipassword.SwipeRevealLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/swipe_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
app:dragEdge="right"
app:mode="same_level"
>
<FrameLayout
android:id="@+id/delete_layout"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@color/btn_delete"
>
<LinearLayout
android:layout_width="70dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="vertical"
>
<ImageView
android:id="@+id/iv_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:tint="@color/white"
app:srcCompat="@drawable/ic_archive_black_24px"
/>
<TextView
android:id="@+id/tv_delete"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:text="@string/to_archive"
android:textColor="@color/white"
/>
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<LinearLayout
android:id="@+id/card_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="14dp"
android:orientation="horizontal"
android:weightSum="1"
>
<TextView
android:id="@+id/SSID"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="0.55"
android:fontFamily="sans-serif"
android:text="Network"
android:textColor="@android:color/black"
android:textSize="16sp"
android:textStyle="bold"
/>
<TextView
android:id="@+id/tv_dot"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="0.05"
android:gravity="center"
android:text="@string/dot"
android:visibility="invisible"
/>
<TextView
android:id="@+id/tv_hide"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_weight="0.3"
android:text="@string/card_hidden"
android:textSize="16sp"
android:visibility="invisible"
/>
<ImageView
android:id="@+id/card_arrow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="0.1"
app:srcCompat="@drawable/ic_keyboard_arrow_down_black_24dp"
/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="14dp"
android:layout_marginEnd="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="2dp"
android:orientation="horizontal"
>
<TextView
android:id="@+id/password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="8dp"
android:fontFamily="sans-serif"
android:text="password"
android:textIsSelectable="true"
android:textSize="14sp"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:text="@string/dot"
/>
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:bufferType="spannable"
android:fontFamily="sans-serif"
android:text="date"
android:textIsSelectable="true"
android:textSize="14sp"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/card_more"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:visibility="gone"
>
<Button
android:id="@+id/ConnectButton"
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
android:background="@color/green_settings"
android:text="@string/connect"
android:textSize="14sp"
style="@style/Base.TextAppearance.AppCompat.Widget.Button.Colored"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="11dp"
android:layout_marginEnd="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
android:background="@color/divider_gray"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
android:baselineAligned="false"
android:orientation="horizontal"
android:weightSum="1"
>
<LinearLayout
android:id="@+id/CopyButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:gravity="center"
android:orientation="horizontal"
>
<ImageView
android:id="@+id/iv_card_copy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:text="@string/copy_password"
android:textSize="14sp"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/ShareButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:gravity="center"
android:orientation="horizontal"
>
<ImageView
android:id="@+id/iv_card_share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:text="@string/share_network"
android:textSize="14sp"
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
</FrameLayout>
</com.rusdelphi.wifipassword.SwipeRevealLayout>
Наконец, нужно создать экземпляр класса RecyclerView в коде активности и инициализировать его. Для правильной работы нужно настроить две вещи: адаптер, который будет предоставлять данные, и менеджер разметки (LayoutManager), который говорит виджету, как нужно отобразить объекты в списке. В результате создание выглядит следующим образом:
private RecyclerView mRecyclerView; private PasswordAdapter mPasswordAdapter; ... mRecyclerView = findViewById(R.id.recyclerView); ... mPasswordAdapter = new PasswordAdapter(this, this, placeholder); mRecyclerView.setLayoutManager(new GridLayoutManager(this, 1)); RecyclerView.ItemAnimator itemAnimator = new DefaultItemAnimator(); itemAnimator.setAddDuration(500); itemAnimator.setRemoveDuration(500); mRecyclerView.setItemAnimator(itemAnimator); mRecyclerView.setAdapter(mPasswordAdapter);
Добавление класса для свайпа
Теперь можно приступить к реализации свайпа для элементов списка. Для этого мы использовали в приложении исходный код библиотеки SwipeRevealLayout, которую вы можете скачать на GitHub по следующей ссылке. Стоит заметить, что способ реализации подобных свайпов в сети достаточно много, этот способ был выбран как самый оптимальный для наших задач.
Как уже говорилось, этот класс используется два FrameLayout, на одном из которых размещается собственно элемента списка, а на втором то, что должно отображаться при свайпе.
Библиотека поддерживает свайпы в любом направлении (слева, справа, сверху, снизу), а также имеет два режима:
- normal — дополнительный элемент рисуется позади основного;
- same level — дополнительный элемент рисуется на том же уровне, что и основной.
В библиотеке также используется дополнительный класс ViewBinderHolder, задачей которого является сохранения состояния свайпа и его восстановление. В нашем приложении в этом нет особой необходимости, поэтому этот класс был выкинут вместе с некоторыми методами основного класса. В результате код класса SwipeRevealLayout получился следующим:
@SuppressLint("RtlHardcoded") public class SwipeRevealLayout extends ViewGroup {
protected static final int STATE_CLOSE = 0;
protected static final int STATE_CLOSING = 1;
protected static final int STATE_OPEN = 2;
protected static final int STATE_OPENING = 3;
protected static final int STATE_DRAGGING = 4;
private static final int DEFAULT_MIN_FLING_VELOCITY = 300; // dp per second
private static final int DEFAULT_MIN_DIST_REQUEST_DISALLOW_PARENT = 1; // dp
public static final int DRAG_EDGE_LEFT = 0x1;
public static final int DRAG_EDGE_RIGHT = 0x1 << 1;
public static final int DRAG_EDGE_TOP = 0x1 << 2;
public static final int DRAG_EDGE_BOTTOM = 0x1 << 3;
public static final int MODE_NORMAL = 0;
public static final int MODE_SAME_LEVEL = 1;
private View mMainView;
private View mSecondaryView;
private Rect mRectMainClose = new Rect();
private Rect mRectMainOpen = new Rect();
private Rect mRectSecClose = new Rect();
private Rect mRectSecOpen = new Rect();
private int mMinDistRequestDisallowParent = 0;
private boolean mIsOpenBeforeInit = false;
private volatile boolean mAborted = false;
private volatile boolean mIsScrolling = false;
private volatile boolean mLockDrag = false;
private int mMinFlingVelocity = DEFAULT_MIN_FLING_VELOCITY;
private int mState = STATE_CLOSE;
private int mMode = MODE_NORMAL;
private int mLastMainLeft = 0;
private int mLastMainTop = 0;
private int mDragEdge = DRAG_EDGE_LEFT;
private ViewDragHelper mDragHelper;
private GestureDetectorCompat mGestureDetector;
private SwipeListener mSwipeListener;
public interface SwipeListener {
void onClosed(SwipeRevealLayout view);
void onOpened(SwipeRevealLayout view);
void onSlide(SwipeRevealLayout view, float slideOffset);
}
public static class SimpleSwipeListener implements SwipeListener {
@Override public void onClosed(SwipeRevealLayout view) {
}
@Override public void onOpened(SwipeRevealLayout view) {
}
@Override public void onSlide(SwipeRevealLayout view, float slideOffset) {
}
}
public SwipeRevealLayout(Context context) {
super(context);
init(context, null);
}
public SwipeRevealLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public SwipeRevealLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
mDragHelper.processTouchEvent(event);
return true;
}
@Override public boolean onInterceptTouchEvent(MotionEvent ev) {
mDragHelper.processTouchEvent(ev);
mGestureDetector.onTouchEvent(ev);
boolean settling = mDragHelper.getViewDragState() == ViewDragHelper.STATE_SETTLING;
boolean idleAfterScrolled =
mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE && mIsScrolling;
return settling || idleAfterScrolled;
}
@Override protected void onFinishInflate() {
super.onFinishInflate();
// get views
if (getChildCount() >= 2) {
mSecondaryView = getChildAt(0);
mMainView = getChildAt(1);
} else if (getChildCount() == 1) {
mMainView = getChildAt(0);
}
}
@SuppressWarnings("ConstantConditions") @Override protected void onLayout(boolean changed, int l,
int t, int r, int b) {
mAborted = false;
for (int index = 0; index < getChildCount(); index++) {
final View child = getChildAt(index);
int left, right, top, bottom;
left = right = top = bottom = 0;
final int minLeft = getPaddingLeft();
final int maxRight = Math.max(r - getPaddingRight() - l, 0);
final int minTop = getPaddingTop();
final int maxBottom = Math.max(b - getPaddingBottom() - t, 0);
int measuredChildHeight = child.getMeasuredHeight();
int measuredChildWidth = child.getMeasuredWidth();
// need to take account if child size is match_parent
final LayoutParams childParams = child.getLayoutParams();
boolean matchParentHeight = false;
boolean matchParentWidth = false;
if (childParams != null) {
matchParentHeight = (childParams.height == LayoutParams.MATCH_PARENT) || (childParams.height
== LayoutParams.FILL_PARENT);
matchParentWidth = (childParams.width == LayoutParams.MATCH_PARENT) || (childParams.width
== LayoutParams.FILL_PARENT);
}
if (matchParentHeight) {
measuredChildHeight = maxBottom - minTop;
childParams.height = measuredChildHeight;
}
if (matchParentWidth) {
measuredChildWidth = maxRight - minLeft;
childParams.width = measuredChildWidth;
}
switch (mDragEdge) {
case DRAG_EDGE_RIGHT:
left = Math.max(r - measuredChildWidth - getPaddingRight() - l, minLeft);
top = Math.min(getPaddingTop(), maxBottom);
right = Math.max(r - getPaddingRight() - l, minLeft);
bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom);
break;
case DRAG_EDGE_LEFT:
left = Math.min(getPaddingLeft(), maxRight);
top = Math.min(getPaddingTop(), maxBottom);
right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight);
bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom);
break;
case DRAG_EDGE_TOP:
left = Math.min(getPaddingLeft(), maxRight);
top = Math.min(getPaddingTop(), maxBottom);
right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight);
bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom);
break;
case DRAG_EDGE_BOTTOM:
left = Math.min(getPaddingLeft(), maxRight);
top = Math.max(b - measuredChildHeight - getPaddingBottom() - t, minTop);
right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight);
bottom = Math.max(b - getPaddingBottom() - t, minTop);
break;
}
child.layout(left, top, right, bottom);
}
// taking account offset when mode is SAME_LEVEL
if (mMode == MODE_SAME_LEVEL) {
switch (mDragEdge) {
case DRAG_EDGE_LEFT:
mSecondaryView.offsetLeftAndRight(-mSecondaryView.getWidth());
break;
case DRAG_EDGE_RIGHT:
mSecondaryView.offsetLeftAndRight(mSecondaryView.getWidth());
break;
case DRAG_EDGE_TOP:
mSecondaryView.offsetTopAndBottom(-mSecondaryView.getHeight());
break;
case DRAG_EDGE_BOTTOM:
mSecondaryView.offsetTopAndBottom(mSecondaryView.getHeight());
}
}
initRects();
if (mIsOpenBeforeInit) {
open(false);
} else {
close(false);
}
mLastMainLeft = mMainView.getLeft();
mLastMainTop = mMainView.getTop();
}
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (getChildCount() < 2) {
throw new RuntimeException("Layout must have two children");
}
final LayoutParams params = getLayoutParams();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int desiredWidth = 0;
int desiredHeight = 0;
// first find the largest child
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
desiredWidth = Math.max(child.getMeasuredWidth(), desiredWidth);
desiredHeight = Math.max(child.getMeasuredHeight(), desiredHeight);
}
// create new measure spec using the largest child width
widthMeasureSpec = MeasureSpec.makeMeasureSpec(desiredWidth, widthMode);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(desiredHeight, heightMode);
final int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
final int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
final LayoutParams childParams = child.getLayoutParams();
if (childParams != null) {
if (childParams.height == LayoutParams.MATCH_PARENT) {
child.setMinimumHeight(measuredHeight);
}
if (childParams.width == LayoutParams.MATCH_PARENT) {
child.setMinimumWidth(measuredWidth);
}
}
measureChild(child, widthMeasureSpec, heightMeasureSpec);
desiredWidth = Math.max(child.getMeasuredWidth(), desiredWidth);
desiredHeight = Math.max(child.getMeasuredHeight(), desiredHeight);
}
// taking accounts of padding
desiredWidth += getPaddingLeft() + getPaddingRight();
desiredHeight += getPaddingTop() + getPaddingBottom();
// adjust desired width
if (widthMode == MeasureSpec.EXACTLY) {
desiredWidth = measuredWidth;
} else {
if (params.width == LayoutParams.MATCH_PARENT) {
desiredWidth = measuredWidth;
}
if (widthMode == MeasureSpec.AT_MOST) {
desiredWidth = (desiredWidth > measuredWidth) ? measuredWidth : desiredWidth;
}
}
// adjust desired height
if (heightMode == MeasureSpec.EXACTLY) {
desiredHeight = measuredHeight;
} else {
if (params.height == LayoutParams.MATCH_PARENT) {
desiredHeight = measuredHeight;
}
if (heightMode == MeasureSpec.AT_MOST) {
desiredHeight = (desiredHeight > measuredHeight) ? measuredHeight : desiredHeight;
}
}
setMeasuredDimension(desiredWidth, desiredHeight);
}
@Override public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
public void open(boolean animation) {
mIsOpenBeforeInit = true;
mAborted = false;
if (animation) {
mState = STATE_OPENING;
mDragHelper.smoothSlideViewTo(mMainView, mRectMainOpen.left, mRectMainOpen.top);
} else {
mState = STATE_OPEN;
mDragHelper.abort();
mMainView.layout(mRectMainOpen.left, mRectMainOpen.top, mRectMainOpen.right,
mRectMainOpen.bottom);
mSecondaryView.layout(mRectSecOpen.left, mRectSecOpen.top, mRectSecOpen.right,
mRectSecOpen.bottom);
}
ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this);
}
public void close(boolean animation) {
mIsOpenBeforeInit = false;
mAborted = false;
if (animation) {
mState = STATE_CLOSING;
mDragHelper.smoothSlideViewTo(mMainView, mRectMainClose.left, mRectMainClose.top);
} else {
mState = STATE_CLOSE;
mDragHelper.abort();
mMainView.layout(mRectMainClose.left, mRectMainClose.top, mRectMainClose.right,
mRectMainClose.bottom);
mSecondaryView.layout(mRectSecClose.left, mRectSecClose.top, mRectSecClose.right,
mRectSecClose.bottom);
}
ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this);
}
public boolean isOpened() {
return (mState == STATE_OPEN);
}
public boolean isClosed() {
return (mState == STATE_CLOSE);
}
private int getMainOpenLeft() {
switch (mDragEdge) {
case DRAG_EDGE_LEFT:
return mRectMainClose.left + mSecondaryView.getWidth();
case DRAG_EDGE_RIGHT:
return mRectMainClose.left - mSecondaryView.getWidth();
case DRAG_EDGE_TOP:
return mRectMainClose.left;
case DRAG_EDGE_BOTTOM:
return mRectMainClose.left;
default:
return 0;
}
}
private int getMainOpenTop() {
switch (mDragEdge) {
case DRAG_EDGE_LEFT:
return mRectMainClose.top;
case DRAG_EDGE_RIGHT:
return mRectMainClose.top;
case DRAG_EDGE_TOP:
return mRectMainClose.top + mSecondaryView.getHeight();
case DRAG_EDGE_BOTTOM:
return mRectMainClose.top - mSecondaryView.getHeight();
default:
return 0;
}
}
private int getSecOpenLeft() {
if (mMode == MODE_NORMAL || mDragEdge == DRAG_EDGE_BOTTOM || mDragEdge == DRAG_EDGE_TOP) {
return mRectSecClose.left;
}
if (mDragEdge == DRAG_EDGE_LEFT) {
return mRectSecClose.left + mSecondaryView.getWidth();
} else {
return mRectSecClose.left - mSecondaryView.getWidth();
}
}
private int getSecOpenTop() {
if (mMode == MODE_NORMAL || mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) {
return mRectSecClose.top;
}
if (mDragEdge == DRAG_EDGE_TOP) {
return mRectSecClose.top + mSecondaryView.getHeight();
} else {
return mRectSecClose.top - mSecondaryView.getHeight();
}
}
private void initRects() {
// close position of main view
mRectMainClose.set(mMainView.getLeft(), mMainView.getTop(), mMainView.getRight(),
mMainView.getBottom());
// close position of secondary view
mRectSecClose.set(mSecondaryView.getLeft(), mSecondaryView.getTop(), mSecondaryView.getRight(),
mSecondaryView.getBottom());
// open position of the main view
mRectMainOpen.set(getMainOpenLeft(), getMainOpenTop(), getMainOpenLeft() + mMainView.getWidth(),
getMainOpenTop() + mMainView.getHeight());
// open position of the secondary view
mRectSecOpen.set(getSecOpenLeft(), getSecOpenTop(),
getSecOpenLeft() + mSecondaryView.getWidth(), getSecOpenTop() + mSecondaryView.getHeight());
}
private void init(Context context, AttributeSet attrs) {
if (attrs != null && context != null) {
TypedArray a =
context.getTheme().obtainStyledAttributes(attrs, R.styleable.SwipeRevealLayout, 0, 0);
mDragEdge = a.getInteger(R.styleable.SwipeRevealLayout_dragEdge, DRAG_EDGE_LEFT);
mMinFlingVelocity =
a.getInteger(R.styleable.SwipeRevealLayout_flingVelocity, DEFAULT_MIN_FLING_VELOCITY);
mMode = a.getInteger(R.styleable.SwipeRevealLayout_mode, MODE_NORMAL);
mMinDistRequestDisallowParent =
a.getDimensionPixelSize(R.styleable.SwipeRevealLayout_minDistRequestDisallowParent,
dpToPx(DEFAULT_MIN_DIST_REQUEST_DISALLOW_PARENT));
}
mDragHelper = ViewDragHelper.create(this, 1.0f, mDragHelperCallback);
mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
mGestureDetector = new GestureDetectorCompat(context, mGestureListener);
}
private final GestureDetector.OnGestureListener mGestureListener =
new GestureDetector.SimpleOnGestureListener() {
boolean hasDisallowed = false;
@Override public boolean onDown(MotionEvent e) {
mIsScrolling = false;
hasDisallowed = false;
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mIsScrolling = true;
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
mIsScrolling = true;
if (getParent() != null) {
boolean shouldDisallow;
if (!hasDisallowed) {
shouldDisallow = getDistToClosestEdge() >= mMinDistRequestDisallowParent;
if (shouldDisallow) {
hasDisallowed = true;
}
} else {
shouldDisallow = true;
}
// disallow parent to intercept touch event so that the layout will work
// properly on RecyclerView or view that handles scroll gesture.
getParent().requestDisallowInterceptTouchEvent(shouldDisallow);
}
return false;
}
};
private int getDistToClosestEdge() {
switch (mDragEdge) {
case DRAG_EDGE_LEFT:
final int pivotRight = mRectMainClose.left + mSecondaryView.getWidth();
return Math.min(mMainView.getLeft() - mRectMainClose.left,
pivotRight - mMainView.getLeft());
case DRAG_EDGE_RIGHT:
final int pivotLeft = mRectMainClose.right - mSecondaryView.getWidth();
return Math.min(mMainView.getRight() - pivotLeft,
mRectMainClose.right - mMainView.getRight());
case DRAG_EDGE_TOP:
final int pivotBottom = mRectMainClose.top + mSecondaryView.getHeight();
return Math.min(mMainView.getBottom() - pivotBottom, pivotBottom - mMainView.getTop());
case DRAG_EDGE_BOTTOM:
final int pivotTop = mRectMainClose.bottom - mSecondaryView.getHeight();
return Math.min(mRectMainClose.bottom - mMainView.getBottom(),
mMainView.getBottom() - pivotTop);
}
return 0;
}
private int getHalfwayPivotHorizontal() {
if (mDragEdge == DRAG_EDGE_LEFT) {
return mRectMainClose.left + mSecondaryView.getWidth() / 2;
} else {
return mRectMainClose.right - mSecondaryView.getWidth() / 2;
}
}
private int getHalfwayPivotVertical() {
if (mDragEdge == DRAG_EDGE_TOP) {
return mRectMainClose.top + mSecondaryView.getHeight() / 2;
} else {
return mRectMainClose.bottom - mSecondaryView.getHeight() / 2;
}
}
private final ViewDragHelper.Callback mDragHelperCallback = new ViewDragHelper.Callback() {
@Override public boolean tryCaptureView(View child, int pointerId) {
mAborted = false;
if (mLockDrag) return false;
mDragHelper.captureChildView(mMainView, pointerId);
return false;
}
@Override public int clampViewPositionVertical(View child, int top, int dy) {
switch (mDragEdge) {
case DRAG_EDGE_TOP:
return Math.max(Math.min(top, mRectMainClose.top + mSecondaryView.getHeight()),
mRectMainClose.top);
case DRAG_EDGE_BOTTOM:
return Math.max(Math.min(top, mRectMainClose.top),
mRectMainClose.top - mSecondaryView.getHeight());
default:
return child.getTop();
}
}
@Override public int clampViewPositionHorizontal(View child, int left, int dx) {
switch (mDragEdge) {
case DRAG_EDGE_RIGHT:
return Math.max(Math.min(left, mRectMainClose.left),
mRectMainClose.left - mSecondaryView.getWidth());
case DRAG_EDGE_LEFT:
return Math.max(Math.min(left, mRectMainClose.left + mSecondaryView.getWidth()),
mRectMainClose.left);
default:
return child.getLeft();
}
}
@Override public void onViewReleased(View releasedChild, float xvel, float yvel) {
final boolean velRightExceeded = pxToDp((int) xvel) >= mMinFlingVelocity;
final boolean velLeftExceeded = pxToDp((int) xvel) <= -mMinFlingVelocity;
final boolean velUpExceeded = pxToDp((int) yvel) <= -mMinFlingVelocity;
final boolean velDownExceeded = pxToDp((int) yvel) >= mMinFlingVelocity;
final int pivotHorizontal = getHalfwayPivotHorizontal();
final int pivotVertical = getHalfwayPivotVertical();
switch (mDragEdge) {
case DRAG_EDGE_RIGHT:
if (velRightExceeded) {
close(true);
} else if (velLeftExceeded) {
open(true);
} else {
if (mMainView.getRight() < pivotHorizontal) {
open(true);
} else {
close(true);
}
}
break;
case DRAG_EDGE_LEFT:
if (velRightExceeded) {
open(true);
} else if (velLeftExceeded) {
close(true);
} else {
if (mMainView.getLeft() < pivotHorizontal) {
close(true);
} else {
open(true);
}
}
break;
case DRAG_EDGE_TOP:
if (velUpExceeded) {
close(true);
} else if (velDownExceeded) {
open(true);
} else {
if (mMainView.getTop() < pivotVertical) {
close(true);
} else {
open(true);
}
}
break;
case DRAG_EDGE_BOTTOM:
if (velUpExceeded) {
open(true);
} else if (velDownExceeded) {
close(true);
} else {
if (mMainView.getBottom() < pivotVertical) {
open(true);
} else {
close(true);
}
}
break;
}
}
@Override public void onEdgeDragStarted(int edgeFlags, int pointerId) {
super.onEdgeDragStarted(edgeFlags, pointerId);
if (mLockDrag) {
return;
}
boolean edgeStartLeft =
(mDragEdge == DRAG_EDGE_RIGHT) && edgeFlags == ViewDragHelper.EDGE_LEFT;
boolean edgeStartRight =
(mDragEdge == DRAG_EDGE_LEFT) && edgeFlags == ViewDragHelper.EDGE_RIGHT;
boolean edgeStartTop =
(mDragEdge == DRAG_EDGE_BOTTOM) && edgeFlags == ViewDragHelper.EDGE_TOP;
boolean edgeStartBottom =
(mDragEdge == DRAG_EDGE_TOP) && edgeFlags == ViewDragHelper.EDGE_BOTTOM;
if (edgeStartLeft || edgeStartRight || edgeStartTop || edgeStartBottom) {
mDragHelper.captureChildView(mMainView, pointerId);
}
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
if (mMode == MODE_SAME_LEVEL) {
if (mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) {
mSecondaryView.offsetLeftAndRight(dx);
} else {
mSecondaryView.offsetTopAndBottom(dy);
}
}
boolean isMoved =
(mMainView.getLeft() != mLastMainLeft) || (mMainView.getTop() != mLastMainTop);
if (mSwipeListener != null && isMoved) {
if (mMainView.getLeft() == mRectMainClose.left
&& mMainView.getTop() == mRectMainClose.top) {
mSwipeListener.onClosed(SwipeRevealLayout.this);
} else if (mMainView.getLeft() == mRectMainOpen.left
&& mMainView.getTop() == mRectMainOpen.top) {
mSwipeListener.onOpened(SwipeRevealLayout.this);
} else {
mSwipeListener.onSlide(SwipeRevealLayout.this, getSlideOffset());
}
}
mLastMainLeft = mMainView.getLeft();
mLastMainTop = mMainView.getTop();
ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this);
}
private float getSlideOffset() {
switch (mDragEdge) {
case DRAG_EDGE_LEFT:
return (float) (mMainView.getLeft() - mRectMainClose.left) / mSecondaryView.getWidth();
case DRAG_EDGE_RIGHT:
return (float) (mRectMainClose.left - mMainView.getLeft()) / mSecondaryView.getWidth();
case DRAG_EDGE_TOP:
return (float) (mMainView.getTop() - mRectMainClose.top) / mSecondaryView.getHeight();
case DRAG_EDGE_BOTTOM:
return (float) (mRectMainClose.top - mMainView.getTop()) / mSecondaryView.getHeight();
default:
return 0;
}
}
@Override public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
final int prevState = mState;
switch (state) {
case ViewDragHelper.STATE_DRAGGING:
mState = STATE_DRAGGING;
break;
case ViewDragHelper.STATE_IDLE:
// drag edge is left or right
if (mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) {
if (mMainView.getLeft() == mRectMainClose.left) {
mState = STATE_CLOSE;
} else {
mState = STATE_OPEN;
}
}
// drag edge is top or bottom
else {
if (mMainView.getTop() == mRectMainClose.top) {
mState = STATE_CLOSE;
} else {
mState = STATE_OPEN;
}
}
break;
}
}
};
private int pxToDp(int px) {
Resources resources = getContext().getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return (int) (px / ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT));
}
private int dpToPx(int dp) {
Resources resources = getContext().getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return (int) (dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT));
}
}
В результате мы добавили в список кнопку, которая появляется при свайпе элемента и позволяет либо добавить сеть в архив, либо удалить её.



