Введение
Для того, чтобы разработать хорошее приложение, недостаточно лишь наполнить его функционалом. Важно также, чтобы интерфейс приложения был удобен и понятен пользователю, а также выглядел презентабельно. Приложение, в котором пользователь не знает, куда нужно нажимать, вряд ли будет пользоваться популярностью.
Отличным инструментом для создания продуманного интерфейса в 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_vpn_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)); } }
В результате мы добавили в список кнопку, которая появляется при свайпе элемента и позволяет либо добавить сеть в архив, либо удалить её.