紧接着上次做的单词记录app,完善删除的功能,实现向左或向右滑动时,从单词列表下方显示删除图标,并持续滑动删除。在用户误删的时候实现一个撤销的操作
处理上次遗留问题:插入数据的跳变反应(不够平滑),是由于添加数据时,LiveData直接呼叫notifyDataSetChanged()函数直接刷新所有函数,造成大的开销。
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.Switch;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import java.sql.RowId;
import java.util.ArrayList;
import java.util.List;
//继承ListAdapter并改写构造方法
public class MyAdapter extends ListAdapter { //<>中表示使用的ViewHolder为自己写的静态内部类MyViewHolder
private boolean useCardView; //是否使用卡片布局视图
private WordViewModel wordViewModel;
MyAdapter(boolean useCardView,WordViewModel wordViewModel) {
super(new DiffUtil.ItemCallback() { //列表数据的差异化处理,是后台异步进行的
@Override
public boolean areItemsTheSame(@NonNull Word oldItem, @NonNull Word newItem) { //比较列表元素是否相同
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull Word oldItem, @NonNull Word newItem) { //比较列表内容是否相同
return (oldItem.getWord().equals(newItem.getWord())
&& oldItem.getChineseMeaning().equals(newItem.getChineseMeaning())
&& oldItem.isChineseInvisible() == newItem.isChineseInvisible());
}
});
this.useCardView = useCardView;
this.wordViewModel = wordViewModel;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
/*
* 当创建ViewHolder时
* 从Layout文件中
* 加载View
* */
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View itemView;
if (useCardView) { //实现加载不同视图
itemView = layoutInflater.inflate(R.layout.cell_card_2,parent,false); //卡片模式界面
} else {
itemView = layoutInflater.inflate(R.layout.cell_normal_2,parent,false); //普通模式界面
}
final MyViewHolder holder = new MyViewHolder(itemView);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Uri uri = Uri.parse("http://dict.youdao.com/w/" + holder.textViewEnglish.getText() + "/#keyfrom=dict2.top");
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(uri);
holder.itemView.getContext().startActivity(intent);
}
});
holder.aSwitchChineseInvisible.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Word word = (Word) holder.itemView.getTag(R.id.word_for_view_holder);//广义object要强制转换
if (isChecked) {
holder.textViewChinese.setVisibility(View.GONE);
word.setChineseInvisible(true);
wordViewModel.updateWords(word);
} else {
holder.textViewChinese.setVisibility(View.VISIBLE);
word.setChineseInvisible(false);
wordViewModel.updateWords(word);
}
}
});
return holder;
}
@Override
public void onBindViewHolder(@NonNull final MyViewHolder holder, int position) {
//绑定逻辑上的关联
final Word word = getItem(position);
//itemView的setTag方法可以获取对象并在其他地方通过getTag获取对应对象
holder.itemView.setTag(R.id.word_for_view_holder,word); //用资源封装保证不冲突
holder.textViewNumber.setText(String.valueOf(position + 1));
holder.textViewEnglish.setText(word.getWord());
holder.textViewChinese.setText(word.getChineseMeaning());
if (word.isChineseInvisible()) {
holder.textViewChinese.setVisibility(View.GONE); //中文不可视
holder.aSwitchChineseInvisible.setChecked(true);//设置开关为开
} else {
holder.textViewChinese.setVisibility(View.VISIBLE);
holder.aSwitchChineseInvisible.setChecked(false);
}
}
static class MyViewHolder extends RecyclerView.ViewHolder {
TextView textViewNumber,textViewEnglish,textViewChinese;
Switch aSwitchChineseInvisible;
MyViewHolder(@NonNull View itemView) { //自带View类型的itemView
super(itemView);
textViewNumber = itemView.findViewById(R.id.textViewNumber);
textViewEnglish = itemView.findViewById(R.id.textViewEnglish);
textViewChinese = itemView.findViewById(R.id.textViewChinese);
aSwitchChineseInvisible = itemView.findViewById(R.id.switchChineseInvisible);
}
}
}
由于改写了Adapter,因此Fragement也需要修改:
filteredWords.observe(requireActivity(), new Observer>() {
@Override
public void onChanged(List words) {
int temp = myAdapter1.getItemCount();
if (temp!=words.size()) {
myAdapter1.submitList(words);
myAdapter2.submitList(words);
}
}
});
可以在recyclerView中设置回调
在Fragement中
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
...
//设置recyclerView的回调
recyclerView.setItemAnimator(new DefaultItemAnimator() {
//复写回调中的函数onAnimationFinished
@Override
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
super.onAnimationFinished(viewHolder);
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
//判空
if (linearLayoutManager != null) {
int firstPosition = linearLayoutManager.findFirstVisibleItemPosition();
int lastPosition = linearLayoutManager.findLastVisibleItemPosition();
//做序号的循环
for (int i = firstPosition; i <= lastPosition; i++) {
MyAdapter.MyViewHolder holder = (MyAdapter.MyViewHolder) recyclerView.findViewHolderForAdapterPosition(i);
//判空
if (holder != null) {
holder.textViewNumber.setText(String.valueOf(i + 1));
}
}
}
}
}); //当添加数据动画结束,设置回调
...
}
为了防止因数据过多数据条目的项目不稳定
还要在Adapter中再复写一个onViewAttachedToWindow方法
@Override
public void onViewAttachedToWindow(@NonNull MyViewHolder holder) {
super.onViewAttachedToWindow(holder);
holder.textViewNumber.setText(String.valueOf(holder.getAdapterPosition() + 1));
}
filteredWords.observe(requireActivity(), new Observer>() {
@Override
public void onChanged(List words) {
int temp = myAdapter1.getItemCount();
if (temp != words.size()) {
recyclerView.smoothScrollBy(0,-200); //设置滑动界面下移200个像素点
myAdapter1.submitList(words);
myAdapter2.submitList(words);
}
}
});
找到LiveData的观察代码
/*
* 对于filteredWords.observe观察
* fragement在观察过程中依赖requireActivity
* 由于Activity不曾被摧毁,
* 所以将Activity作为观察的owner
* 会出现界面上的一个重叠
* 需要解决将getActivity()修改采用getViewLifecycleOwner()
* */
filteredWords.observe(getViewLifecycleOwner(), new Observer>() {
@Override
public void onChanged(List words) {
int temp = myAdapter1.getItemCount(); //观察的代码与下面的一样,可复制
if (temp != words.size()) {
myAdapter1.submitList(words);
myAdapter2.submitList(words);
}
}
});
制作一个列表变动的Helper
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0,ItemTouchHelper.START | ItemTouchHelper.END) { //滑动的两个参数分别为拖动方向和滑动方向,可以选中跳到定义查看
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
Word wordToDelete = allWords.get(viewHolder.getAdapterPosition()); //用allWords代替LiveData避免警告
wordViewModel.deleteWords(wordToDelete); //完成删除操作
}
}).attachToRecyclerView(recyclerView); //附加recyclerView生效
为了防止LiveData异步机制造成的无法实时查看信息,使用
private List allWords; //为了处理LiveData造成的异步获取数据造成的无法实时返回结果
并将其带入观察和查询中
allWords = words;
制作撤消功能要使用到Snackbar
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0,ItemTouchHelper.START | ItemTouchHelper.END) { //滑动的两个参数分别为拖动方向和滑动方向,可以选中跳到定义查看
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
final Word wordToDelete = allWords.get(viewHolder.getAdapterPosition()); //用allWords代替LiveData避免警告,获取删除节点
wordViewModel.deleteWords(wordToDelete); //完成删除操作
Snackbar.make(requireActivity().findViewById(R.id.wordsFragementView),"删除了一个词汇",Snackbar.LENGTH_SHORT)
.setAction("撤销", new View.OnClickListener() {
@Override
public void onClick(View v) {
wordViewModel.insertWords(wordToDelete); //将上面的数据重新插入回数据库中
}
}).show();
}
}).attachToRecyclerView(recyclerView); //附加recyclerView生效
效果如下:
点击撤销按钮即可恢复数据:
对于撤销信息与添加按钮之间的协调问题
可以将布局改为CoordinatorLayout(协调布局)即可
制作删除背景下的删除图标,人性化提示删除功能:
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,ItemTouchHelper.START | ItemTouchHelper.END) { //滑动的两个参数分别为拖动方向和滑动方向,可以选中跳到定义查看
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { //滑动删除操作代码
final Word wordToDelete = allWords.get(viewHolder.getAdapterPosition()); //用allWords代替LiveData避免警告,获取删除节点
wordViewModel.deleteWords(wordToDelete); //完成删除操作
Snackbar.make(requireActivity().findViewById(R.id.wordsFragementView),"删除了一个词汇",Snackbar.LENGTH_SHORT)
.setAction("撤销", new View.OnClickListener() {
@Override
public void onClick(View v) {
undoAction = true;
wordViewModel.insertWords(wordToDelete); //将上面的数据重新插入回数据库中
}
}).show();
}
//制作滑动下的删除图标
Drawable icon = ContextCompat.getDrawable(requireActivity(),R.drawable.ic_delete_black_24dp);
Drawable background = new ColorDrawable(Color.LTGRAY);
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
View itemView = viewHolder.itemView;
int iconMargin = (itemView.getHeight() - icon.getIntrinsicHeight()) / 2;
int iconLeft,iconRight,iconTop,iconBottom;
int backTop,backBottom,backLeft,backRight;
backTop = itemView.getTop();
backBottom = itemView.getBottom();
iconTop = itemView.getTop() + (itemView.getHeight() - icon.getIntrinsicHeight()) / 2;
iconBottom = iconTop + icon.getIntrinsicHeight();
if (dX > 0) {
backLeft = itemView.getLeft();
backRight = itemView.getLeft() + (int) dX;
background.setBounds(backLeft,backTop,backRight,backBottom);
iconLeft = itemView.getLeft() + iconMargin;
iconRight = iconLeft + icon.getIntrinsicWidth();
icon.setBounds(iconLeft,iconTop,iconRight,iconBottom);
} else if (dX < 0){
backRight = itemView.getRight();
backLeft = itemView.getRight() + (int) dX;
background.setBounds(backLeft,backTop,backRight,backBottom);
iconRight = itemView.getRight() - iconMargin;
iconLeft = iconRight - icon.getIntrinsicWidth();
icon.setBounds(iconLeft,iconTop,iconRight,iconBottom);
} else {
background.setBounds(0,0,0,0);
icon.draw(c);
}
background.draw(c);
icon.draw(c);
//滑动删除完成
}
}).attachToRecyclerView(recyclerView); //附加recyclerView生效
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0,ItemTouchHelper.START | ItemTouchHelper.END) { //滑动的两个参数分别为拖动方向和滑动方向,可以选中跳到定义查看
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
//先获取位置对象的两个参数ViewHolder,target
Word worfFrom = allWords.get(viewHolder.getAdapterPosition());
Word wordTo = allWords.get(target.getAdapterPosition());
/*
* ID交换三部曲
* */
int idTemp = worfFrom.getId();
worfFrom.setId(wordTo.getId());
wordTo.setId(idTemp);
wordViewModel.updateWords(worfFrom,wordTo);
myAdapter1.notifyItemMoved(viewHolder.getAdapterPosition(),target.getAdapterPosition());
myAdapter2.notifyItemMoved(viewHolder.getAdapterPosition(),target.getAdapterPosition());
return false;
}
但是使用上述方法时,对ViewModel层面上的直接操作,会导致因操作上的不正当使用(快速拖动等)造成数据上的错乱,对于拖动排序的功能:平时应该在菜单栏中设置排序,并通过右侧的排序按钮(类似三条杠杠)来进行操作。