From 69911499e69b7abc7e93678063a2b1c75a052c48 Mon Sep 17 00:00:00 2001 From: Eric Kuck Date: Fri, 6 Jul 2018 16:02:48 -0500 Subject: Switch from ListView to RecyclerView Signed-off-by: Eric Kuck --- .../android/databinding/BindingAdapters.java | 36 +---- .../databinding/ObservableKeyedListAdapter.java | 133 ------------------ .../ObservableKeyedRecyclerViewAdapter.java | 17 ++- .../android/fragment/TunnelListFragment.java | 148 +++++++++++---------- .../res/drawable/list_item_background_anim.xml | 1 + app/src/main/res/layout/tunnel_list_fragment.xml | 9 +- 6 files changed, 107 insertions(+), 237 deletions(-) delete mode 100644 app/src/main/java/com/wireguard/android/databinding/ObservableKeyedListAdapter.java diff --git a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java b/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java index b7c44116..b3a8ae25 100644 --- a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java +++ b/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java @@ -13,10 +13,10 @@ import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.InputFilter; import android.widget.LinearLayout; -import android.widget.ListView; import android.widget.TextView; import com.wireguard.android.R; +import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler; import com.wireguard.android.util.ObservableKeyedList; import com.wireguard.android.widget.ToggleSwitch; import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener; @@ -67,37 +67,11 @@ public final class BindingAdapters { listener.setList(newList); } - @BindingAdapter({"items", "layout"}) - public static > - void setItems(final ListView view, - final ObservableKeyedList oldList, final int oldLayoutId, - final ObservableKeyedList newList, final int newLayoutId) { - if (oldList == newList && oldLayoutId == newLayoutId) - return; - // The ListAdapter interface is not generic, so this cannot be checked. - @SuppressWarnings("unchecked") ObservableKeyedListAdapter adapter = - (ObservableKeyedListAdapter) view.getAdapter(); - // If the layout changes, any existing adapter must be replaced. - if (adapter != null && oldList != null && oldLayoutId != newLayoutId) { - adapter.setList(null); - adapter = null; - } - // Avoid setting an adapter when there is no new list or layout. - if (newList == null || newLayoutId == 0) - return; - if (adapter == null) { - adapter = new ObservableKeyedListAdapter<>(view.getContext(), newLayoutId, newList); - view.setAdapter(adapter); - } - // Either the list changed, or this is an entirely new listener because the layout changed. - adapter.setList(newList); - } - - @BindingAdapter({"items", "layout"}) + @BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"}) public static > void setItems(final RecyclerView view, - final ObservableKeyedList oldList, final int oldLayoutId, - final ObservableKeyedList newList, final int newLayoutId) { + final ObservableKeyedList oldList, final int oldLayoutId, final RowConfigurationHandler oldRowConfigurationHandler, + final ObservableKeyedList newList, final int newLayoutId, final RowConfigurationHandler newRowConfigurationHandler) { if (view.getLayoutManager() == null) view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false)); @@ -118,6 +92,8 @@ public final class BindingAdapters { adapter = new ObservableKeyedRecyclerViewAdapter<>(view.getContext(), newLayoutId, newList); view.setAdapter(adapter); } + + adapter.setRowConfigurationHandler(newRowConfigurationHandler); // Either the list changed, or this is an entirely new listener because the layout changed. adapter.setList(newList); } diff --git a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedListAdapter.java b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedListAdapter.java deleted file mode 100644 index 9cf9490f..00000000 --- a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedListAdapter.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright © 2018 Samuel Holland - * Copyright © 2018 Jason A. Donenfeld . All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.android.databinding; - -import android.content.Context; -import android.databinding.DataBindingUtil; -import android.databinding.ObservableList; -import android.databinding.ViewDataBinding; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; - -import com.wireguard.android.BR; -import com.wireguard.util.Keyed; -import com.wireguard.android.util.ObservableKeyedList; - -import java.lang.ref.WeakReference; - -/** - * A generic {@code ListAdapter} backed by a {@code ObservableKeyedList}. - */ - -class ObservableKeyedListAdapter> extends BaseAdapter { - private final OnListChangedCallback callback = new OnListChangedCallback<>(this); - private final int layoutId; - private final LayoutInflater layoutInflater; - private ObservableKeyedList list; - - ObservableKeyedListAdapter(final Context context, final int layoutId, - final ObservableKeyedList list) { - this.layoutId = layoutId; - layoutInflater = LayoutInflater.from(context); - setList(list); - } - - @Override - public int getCount() { - return list != null ? list.size() : 0; - } - - @Override - public E getItem(final int position) { - if (list == null || position < 0 || position >= list.size()) - return null; - return list.get(position); - } - - @Override - public long getItemId(final int position) { - final K key = getKey(position); - return key != null ? key.hashCode() : -1; - } - - private K getKey(final int position) { - final E item = getItem(position); - return item != null ? item.getKey() : null; - } - - @Override - public View getView(final int position, final View convertView, final ViewGroup parent) { - ViewDataBinding binding = DataBindingUtil.getBinding(convertView); - if (binding == null) - binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false); - binding.setVariable(BR.collection, list); - binding.setVariable(BR.key, getKey(position)); - binding.setVariable(BR.item, getItem(position)); - binding.executePendingBindings(); - return binding.getRoot(); - } - - @Override - public boolean hasStableIds() { - return true; - } - - void setList(final ObservableKeyedList newList) { - if (list != null) - list.removeOnListChangedCallback(callback); - list = newList; - if (list != null) { - list.addOnListChangedCallback(callback); - } - notifyDataSetChanged(); - } - - private static final class OnListChangedCallback> - extends ObservableList.OnListChangedCallback> { - - private final WeakReference> weakAdapter; - - private OnListChangedCallback(final ObservableKeyedListAdapter adapter) { - weakAdapter = new WeakReference<>(adapter); - } - - @Override - public void onChanged(final ObservableList sender) { - final ObservableKeyedListAdapter adapter = weakAdapter.get(); - if (adapter != null) - adapter.notifyDataSetChanged(); - else - sender.removeOnListChangedCallback(this); - } - - @Override - public void onItemRangeChanged(final ObservableList sender, final int positionStart, - final int itemCount) { - onChanged(sender); - } - - @Override - public void onItemRangeInserted(final ObservableList sender, final int positionStart, - final int itemCount) { - onChanged(sender); - } - - @Override - public void onItemRangeMoved(final ObservableList sender, final int fromPosition, - final int toPosition, final int itemCount) { - onChanged(sender); - } - - @Override - public void onItemRangeRemoved(final ObservableList sender, final int positionStart, - final int itemCount) { - onChanged(sender); - } - } -} diff --git a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java index 4a5ac3d2..79168e48 100644 --- a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java +++ b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java @@ -14,6 +14,7 @@ import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.Adapter; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import com.wireguard.android.BR; @@ -26,12 +27,13 @@ import java.lang.ref.WeakReference; * A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}. */ -class ObservableKeyedRecyclerViewAdapter> extends Adapter { +public class ObservableKeyedRecyclerViewAdapter> extends Adapter { private final OnListChangedCallback callback = new OnListChangedCallback<>(this); private final int layoutId; private final LayoutInflater layoutInflater; private ObservableKeyedList list; + private RowConfigurationHandler rowConfigurationHandler; ObservableKeyedRecyclerViewAdapter(final Context context, final int layoutId, final ObservableKeyedList list) { @@ -67,12 +69,17 @@ class ObservableKeyedRecyclerViewAdapter> extend return new ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false)); } + @SuppressWarnings("unchecked") @Override public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) { holder.binding.setVariable(BR.collection, list); holder.binding.setVariable(BR.key, getKey(position)); holder.binding.setVariable(BR.item, getItem(position)); holder.binding.executePendingBindings(); + + if (rowConfigurationHandler != null) { + rowConfigurationHandler.onConfigureRow(holder.binding.getRoot(), getItem(position), position); + } } void setList(final ObservableKeyedList newList) { @@ -85,6 +92,10 @@ class ObservableKeyedRecyclerViewAdapter> extend notifyDataSetChanged(); } + void setRowConfigurationHandler(final RowConfigurationHandler rowConfigurationHandler) { + this.rowConfigurationHandler = rowConfigurationHandler; + } + private static final class OnListChangedCallback> extends ObservableList.OnListChangedCallback> { @@ -138,4 +149,8 @@ class ObservableKeyedRecyclerViewAdapter> extend } } + public interface RowConfigurationHandler { + void onConfigureRow(View view, T item, int position); + } + } diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java index 403e458b..66c1d7b9 100644 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java @@ -16,26 +16,21 @@ import android.net.Uri; import android.os.Bundle; import android.provider.OpenableColumns; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.view.ActionMode; import android.util.Log; -import android.util.SparseBooleanArray; -import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; -import android.view.MotionEvent; import android.view.View; -import android.view.View.OnTouchListener; import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.AbsListView.MultiChoiceModeListener; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemLongClickListener; import com.wireguard.android.Application; import com.wireguard.android.R; import com.wireguard.android.activity.TunnelCreatorActivity; +import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter; import com.wireguard.android.databinding.TunnelListFragmentBinding; import com.wireguard.android.model.Tunnel; import com.wireguard.android.util.ExceptionLoggers; @@ -47,13 +42,13 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java9.util.concurrent.CompletableFuture; -import java9.util.stream.Collectors; -import java9.util.stream.IntStream; import java9.util.stream.StreamSupport; /** @@ -64,8 +59,7 @@ public class TunnelListFragment extends BaseFragment { private static final int REQUEST_IMPORT = 1; private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName(); - private final MultiChoiceModeListener actionModeListener = new ActionModeListener(); - private final ListViewCallbacks listViewCallbacks = new ListViewCallbacks(); + private final ActionModeListener actionModeListener = new ActionModeListener(); private ActionMode actionMode; private TunnelListFragmentBinding binding; @@ -182,20 +176,19 @@ public class TunnelListFragment extends BaseFragment { } } - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - + @SuppressLint("ClickableViewAccessibility") @Override public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); binding = TunnelListFragmentBinding.inflate(inflater, container, false); - binding.tunnelList.setMultiChoiceModeListener(actionModeListener); - binding.tunnelList.setOnItemClickListener(listViewCallbacks); - binding.tunnelList.setOnItemLongClickListener(listViewCallbacks); - binding.tunnelList.setOnTouchListener(listViewCallbacks); + + binding.tunnelList.setOnTouchListener((view, motionEvent) -> { + if (binding != null) { + binding.createMenu.collapse(); + } + return false; + }); binding.executePendingBindings(); return binding.getRoot(); } @@ -276,38 +269,54 @@ public class TunnelListFragment extends BaseFragment { super.onViewStateRestored(savedInstanceState); binding.setFragment(this); binding.setTunnels(Application.getTunnelManager().getTunnels()); + binding.setRowConfigurationHandler(new ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler() { + @Override + public void onConfigureRow(View view, Tunnel tunnel, int position) { + view.setOnClickListener(clicked -> { + if (actionMode == null) { + setSelectedTunnel(tunnel); + } else { + actionModeListener.toggleItemChecked(position); + } + }); + view.setOnLongClickListener(clicked -> { + actionModeListener.toggleItemChecked(position); + return true; + }); + + view.setActivated(actionModeListener.checkedItems.contains(position)); + } + }); } - private final class ActionModeListener implements MultiChoiceModeListener { - private Resources resources; - private AbsListView tunnelList; + private final class ActionModeListener implements ActionMode.Callback { + private final Set checkedItems = new HashSet<>(); - private IntStream getCheckedPositions() { - final SparseBooleanArray checkedItemPositions = tunnelList.getCheckedItemPositions(); - return IntStream.range(0, checkedItemPositions.size()) - .filter(checkedItemPositions::valueAt) - .map(checkedItemPositions::keyAt); - } + private Resources resources; @Override public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) { switch (item.getItemId()) { case R.id.menu_action_delete: - // Must operate in two steps: positions change once we start deleting things. - final List tunnelsToDelete = getCheckedPositions() - .mapToObj(pos -> (Tunnel) tunnelList.getItemAtPosition(pos)) - .collect(Collectors.toList()); + List tunnelsToDelete = new ArrayList<>(); + for (Integer position : checkedItems) { + tunnelsToDelete.add(Application.getTunnelManager().getTunnels().get(position)); + } + final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete) .map(Tunnel::delete) .toArray(CompletableFuture[]::new); CompletableFuture.allOf(futures) .thenApply(x -> futures.length) .whenComplete(TunnelListFragment.this::onTunnelDeletionFinished); + + checkedItems.clear(); mode.finish(); return true; case R.id.menu_action_select_all: - for (int i = 0; i < tunnelList.getAdapter().getCount(); ++i) - tunnelList.setItemChecked(i, true); + for (int i = 0; i < Application.getTunnelManager().getTunnels().size(); ++i) { + setItemChecked(i, true); + } return true; default: return false; @@ -317,9 +326,9 @@ public class TunnelListFragment extends BaseFragment { @Override public boolean onCreateActionMode(final ActionMode mode, final Menu menu) { actionMode = mode; - if (getActivity() != null) + if (getActivity() != null) { resources = getActivity().getResources(); - tunnelList = binding.tunnelList; + } mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu); return true; } @@ -328,12 +337,31 @@ public class TunnelListFragment extends BaseFragment { public void onDestroyActionMode(final ActionMode mode) { actionMode = null; resources = null; + + checkedItems.clear(); + binding.tunnelList.getAdapter().notifyDataSetChanged(); } - @Override - public void onItemCheckedStateChanged(final ActionMode mode, final int position, - final long id, final boolean checked) { - updateTitle(mode); + void toggleItemChecked(int position) { + setItemChecked(position, !checkedItems.contains(position)); + } + + void setItemChecked(int position, boolean checked) { + if (checked) { + checkedItems.add(position); + } else { + checkedItems.remove(position); + } + + if (actionMode == null && !checkedItems.isEmpty() && getActivity() != null) { + ((AppCompatActivity) getActivity()).startSupportActionMode(this); + } else if (actionMode != null && checkedItems.isEmpty()) { + actionMode.finish(); + } + + binding.tunnelList.getAdapter().notifyItemChanged(position); + + updateTitle(actionMode); } @Override @@ -342,8 +370,12 @@ public class TunnelListFragment extends BaseFragment { return false; } - private void updateTitle(final ActionMode mode) { - final int count = (int) getCheckedPositions().count(); + private void updateTitle(@Nullable final ActionMode mode) { + if (mode == null) { + return; + } + + final int count = checkedItems.size(); if (count == 0) { mode.setTitle(""); } else { @@ -352,30 +384,4 @@ public class TunnelListFragment extends BaseFragment { } } - private final class ListViewCallbacks - implements OnItemClickListener, OnItemLongClickListener, OnTouchListener { - @Override - public void onItemClick(final AdapterView parent, final View view, - final int position, final long id) { - setSelectedTunnel((Tunnel) parent.getItemAtPosition(position)); - } - - @Override - public boolean onItemLongClick(final AdapterView parent, final View view, - final int position, final long id) { - if (actionMode != null) - return false; - if (binding != null) - binding.tunnelList.setItemChecked(position, true); - return true; - } - - @Override - @SuppressLint("ClickableViewAccessibility") - public boolean onTouch(final View view, final MotionEvent motionEvent) { - if (binding != null) - binding.createMenu.collapse(); - return false; - } - } } diff --git a/app/src/main/res/drawable/list_item_background_anim.xml b/app/src/main/res/drawable/list_item_background_anim.xml index 98bfded9..213130c7 100644 --- a/app/src/main/res/drawable/list_item_background_anim.xml +++ b/app/src/main/res/drawable/list_item_background_anim.xml @@ -2,4 +2,5 @@ + diff --git a/app/src/main/res/layout/tunnel_list_fragment.xml b/app/src/main/res/layout/tunnel_list_fragment.xml index 29b6fe08..cad2e094 100644 --- a/app/src/main/res/layout/tunnel_list_fragment.xml +++ b/app/src/main/res/layout/tunnel_list_fragment.xml @@ -10,6 +10,10 @@ name="fragment" type="com.wireguard.android.fragment.TunnelListFragment" /> + + @@ -21,13 +25,14 @@ android:layout_height="match_parent" android:background="?android:attr/colorBackground"> - + app:layout="@{@layout/tunnel_list_item}" + app:configurationHandler="@{rowConfigurationHandler}" />