From 80f7e475112c7d674315d04960ca851c79970cd2 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Wed, 22 Oct 2025 00:21:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0fab=E9=9A=8FRecyclerView?= =?UTF-8?q?=E6=BB=91=E5=8A=A8=E9=9A=90=E8=97=8F=E6=98=BE=E7=A4=BA=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B7=BB=E5=8A=A0=E8=81=94=E7=B3=BB=E4=BA=BA?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=EF=BC=8C=E5=AE=9E=E7=8E=B0=E8=81=94=E7=B3=BB?= =?UTF-8?q?=E4=BA=BA=E6=8B=96=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 12 ++ .../contact/add/ContactAddActivity.java | 65 ++++++++++ .../contact/add/ContactAddViewModel.java | 33 +++++ .../contact/edit/ContactEditActivity.java | 43 +++++++ .../contact/edit/ContactEditViewModel.java | 8 ++ .../contact/list/ContactListActivity.java | 98 +++++++++++++++ .../contact/list/ContactListViewModel.java | 45 +++++++ .../dialer/adapter/ContactInfoAdapter.java | 106 ++++++++++++++++ .../com/ttstd/dialer/contact/AppDatabase.java | 4 +- .../com/ttstd/dialer/contact/Contact.java | 10 ++ .../com/ttstd/dialer/contact/ContactDao.java | 10 +- .../dialer/contact/ContactRepository.java | 20 +-- .../dialer/filter/NoSpaceInputFilter.java | 25 ++++ .../dialer/fragment/home/HomeFragment.java | 14 +-- .../com/ttstd/dialer/view/FabBehavior.java | 117 ++++++++++++++++++ .../dialer/view/ItemTouchHelperCallback.java | 54 ++++++++ .../dialer/view/ScrollAwareFABBehavior.java | 87 +++++++++++++ app/src/main/res/anim/fab_in.xml | 5 + app/src/main/res/anim/fab_out.xml | 5 + .../main/res/drawable/ic_default_avatar.xml | 12 ++ .../main/res/layout/activity_contact_add.xml | 114 +++++++++++++++++ .../main/res/layout/activity_contact_edit.xml | 17 +++ .../main/res/layout/activity_contact_list.xml | 37 ++++++ app/src/main/res/layout/item_contact.xml | 65 ++++++++++ app/src/main/res/values/strings.xml | 1 + 25 files changed, 982 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddActivity.java create mode 100644 app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java create mode 100644 app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditActivity.java create mode 100644 app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditViewModel.java create mode 100644 app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java create mode 100644 app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java create mode 100644 app/src/main/java/com/ttstd/dialer/adapter/ContactInfoAdapter.java create mode 100644 app/src/main/java/com/ttstd/dialer/filter/NoSpaceInputFilter.java create mode 100644 app/src/main/java/com/ttstd/dialer/view/FabBehavior.java create mode 100644 app/src/main/java/com/ttstd/dialer/view/ItemTouchHelperCallback.java create mode 100644 app/src/main/java/com/ttstd/dialer/view/ScrollAwareFABBehavior.java create mode 100644 app/src/main/res/anim/fab_in.xml create mode 100644 app/src/main/res/anim/fab_out.xml create mode 100644 app/src/main/res/drawable/ic_default_avatar.xml create mode 100644 app/src/main/res/layout/activity_contact_add.xml create mode 100644 app/src/main/res/layout/activity_contact_edit.xml create mode 100644 app/src/main/res/layout/activity_contact_list.xml create mode 100644 app/src/main/res/layout/item_contact.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 000bd48..8af9375 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,18 @@ + + + { + + @Override + public boolean setNightMode() { + return true; + } + + @Override + public boolean setfitWindow() { + return true; + } + + @Override + protected int getLayoutId() { + return R.layout.activity_contact_add; + } + + @Override + protected void initDataBinding() { + mViewModel.setContext(this); + mViewModel.setVDBinding(mViewDataBinding); + mViewModel.setLifecycle(getLifecycleSubject()); + mViewDataBinding.setClick(new BtnClick()); + } + + @Override + protected void initView() { + mViewDataBinding.etName.setFilters(new InputFilter[]{new NoSpaceInputFilter()}); + mViewDataBinding.etPhone.setFilters(new InputFilter[]{new NoSpaceInputFilter()}); + } + + @Override + protected void initData() { + mViewModel.mIntegerMutableLiveData.observe(this, new Observer() { + @Override + public void onChanged(Long aLong) { + if (aLong > 0) { + finish(); + } + } + }); + } + + public class BtnClick { + public void save(View view) { + String name = mViewDataBinding.etName.getText().toString(); + String phone = mViewDataBinding.etPhone.getText().toString(); + Contact contact = new Contact(name, phone); + mViewModel.insert(contact); + } + } +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java new file mode 100644 index 0000000..7704c58 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java @@ -0,0 +1,33 @@ +package com.ttstd.dialer.activity.contact.add; + +import android.content.Context; +import android.util.Log; + +import androidx.lifecycle.MutableLiveData; + +import com.trello.rxlifecycle4.android.ActivityEvent; +import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.contact.Contact; +import com.ttstd.dialer.contact.ContactRepository; +import com.ttstd.dialer.databinding.ActivityContactAddBinding; + +import java.util.List; + +public class ContactAddViewModel extends BaseViewModel { + private static final String TAG = "ContactAddViewModel"; + private ContactRepository mRepository; + + @Override + public void setContext(Context context) { + super.setContext(context); + mRepository = new ContactRepository(context); + } + + public MutableLiveData mIntegerMutableLiveData = new MutableLiveData<>(); + + public void insert(Contact contact) { + Log.e(TAG, "insert: " ); + mIntegerMutableLiveData.setValue(mRepository.insert(contact)); + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditActivity.java b/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditActivity.java new file mode 100644 index 0000000..6fe62a3 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditActivity.java @@ -0,0 +1,43 @@ +package com.ttstd.dialer.activity.contact.edit; + +import com.ttstd.dialer.R; +import com.ttstd.dialer.base.mvvm.BaseMvvmActivity; +import com.ttstd.dialer.databinding.ActivityContactEditBinding; + +public class ContactEditActivity extends BaseMvvmActivity { + + @Override + public boolean setNightMode() { + return true; + } + + @Override + protected int getLayoutId() { + return R.layout.activity_contact_edit; + } + + @Override + protected void initDataBinding() { + mViewModel.setContext(this); + mViewModel.setVDBinding(mViewDataBinding); + mViewModel.setLifecycle(getLifecycleSubject()); + mViewDataBinding.setClick(new BtnClick()); + } + + @Override + protected void initView() { + + } + + @Override + protected void initData() { + + } + + + + public class BtnClick{ + + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditViewModel.java new file mode 100644 index 0000000..45fcc9e --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/edit/ContactEditViewModel.java @@ -0,0 +1,8 @@ +package com.ttstd.dialer.activity.contact.edit; + +import com.trello.rxlifecycle4.android.ActivityEvent; +import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.databinding.ActivityContactEditBinding; + +public class ContactEditViewModel extends BaseViewModel { +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java new file mode 100644 index 0000000..4062c49 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java @@ -0,0 +1,98 @@ +package com.ttstd.dialer.activity.contact.list; + +import android.content.Intent; +import android.util.Log; +import android.view.View; + +import androidx.lifecycle.Observer; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.ttstd.dialer.R; +import com.ttstd.dialer.activity.contact.add.ContactAddActivity; +import com.ttstd.dialer.adapter.ContactInfoAdapter; +import com.ttstd.dialer.base.mvvm.BaseMvvmActivity; +import com.ttstd.dialer.contact.Contact; +import com.ttstd.dialer.databinding.ActivityContactListBinding; +import com.ttstd.dialer.view.ItemTouchHelperCallback; + +import java.util.List; + +public class ContactListActivity extends BaseMvvmActivity { + + private static final String TAG = "ContactListActivity"; + + private ContactInfoAdapter mContactInfoAdapter; + + @Override + public boolean setNightMode() { + return true; + } + + @Override + public boolean setfitWindow() { + return true; + } + + @Override + protected int getLayoutId() { + return R.layout.activity_contact_list; + } + + @Override + protected void initDataBinding() { + mViewModel.setContext(this); + mViewModel.setVDBinding(mViewDataBinding); + mViewModel.setLifecycle(getLifecycleSubject()); + mViewDataBinding.setClick(new BtnClick()); + } + + @Override + protected void initView() { + mContactInfoAdapter = new ContactInfoAdapter(); + mContactInfoAdapter.setItemMoveCallback(new ContactInfoAdapter.ItemMoveCallback() { + @Override + public void onItemMove(int fromPosition, int toPosition) { + Log.e(TAG, "onItemMove: " ); + } + + @Override + public void onItemRemoved(int position) { + Log.e(TAG, "onItemRemoved: " ); + } + }); + // 设置ItemTouchHelper,实现拖动和滑动删除 + ItemTouchHelper.Callback callback = new ItemTouchHelperCallback(mContactInfoAdapter); + ItemTouchHelper touchHelper = new ItemTouchHelper(callback); + touchHelper.attachToRecyclerView(mViewDataBinding.recyclerView); + + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); + linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); + mViewDataBinding.recyclerView.setLayoutManager(linearLayoutManager); + mViewDataBinding.recyclerView.setAdapter(mContactInfoAdapter); + } + + @Override + protected void initData() { + mViewModel.mContactListData.observe(this, new Observer>() { + @Override + public void onChanged(List contacts) { + Log.e(TAG, "onChanged: " + contacts); + mContactInfoAdapter.setContacts(contacts); + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + mViewModel.getAllContacts(); + } + + public class BtnClick { + public void addContact(View view) { + startActivity(new Intent(ContactListActivity.this, ContactAddActivity.class)); + } + } +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java new file mode 100644 index 0000000..52c20ed --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java @@ -0,0 +1,45 @@ +package com.ttstd.dialer.activity.contact.list; + +import android.content.Context; + +import androidx.lifecycle.MutableLiveData; + +import com.trello.rxlifecycle4.android.ActivityEvent; +import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.contact.Contact; +import com.ttstd.dialer.contact.ContactRepository; +import com.ttstd.dialer.databinding.ActivityContactListBinding; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class ContactListViewModel extends BaseViewModel { + private static final String TAG = "ContactListViewModel"; + private ContactRepository mRepository; + + @Override + public void setContext(Context context) { + super.setContext(context); + mRepository = new ContactRepository(context); + } + + public MutableLiveData> mContactListData = new MutableLiveData<>(); + + public void getAllContacts() { + List contacts = mRepository.getAllContacts(); + List sorted = contacts.stream().sorted(new Comparator() { + @Override + public int compare(Contact o1, Contact o2) { + return Integer.compare(o1.getSort(), o2.getSort()); + } + }).collect(Collectors.toList()); + + mContactListData.setValue(sorted); + } + + public List searchContacts(String query) { + return mRepository.searchContacts(query); + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/adapter/ContactInfoAdapter.java b/app/src/main/java/com/ttstd/dialer/adapter/ContactInfoAdapter.java new file mode 100644 index 0000000..b3d63e4 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/adapter/ContactInfoAdapter.java @@ -0,0 +1,106 @@ +package com.ttstd.dialer.adapter; + +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.RecyclerView; + +import com.shehuan.niv.NiceImageView; +import com.ttstd.dialer.R; +import com.ttstd.dialer.contact.Contact; + +import java.util.Collections; +import java.util.List; + +public class ContactInfoAdapter extends RecyclerView.Adapter { + private static final String TAG = "ContactInfoAdapter"; + + private FragmentActivity mContext; + + private List mContacts; + + public void setContacts(List contacts) { + mContacts = contacts; + notifyDataSetChanged(); + } + + public void setItemMoveCallback(ItemMoveCallback itemMoveCallback) { + mItemMoveCallback = itemMoveCallback; + } + + private ItemMoveCallback mItemMoveCallback; + + public interface ItemMoveCallback { + void onItemMove(int fromPosition, int toPosition); + + void onItemRemoved(int position); + } + + @NonNull + @Override + public ContactInfoHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + mContext = (FragmentActivity) parent.getContext(); + return new ContactInfoHolder(LayoutInflater.from(mContext).inflate(R.layout.item_contact, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull ContactInfoHolder holder, int position) { + Contact contact = mContacts.get(position); + String name = contact.getName(); + holder.tv_name.setText(name); + String phone = contact.getPhoneNumber(); + holder.tv_phone.setText(phone); + } + + @Override + public int getItemCount() { + return mContacts == null ? 0 : mContacts.size(); + } + + + // 处理拖动交换位置 + public void onItemMove(int fromPosition, int toPosition) { + Log.e(TAG, "onItemMove: fromPosition = " + fromPosition); + Log.e(TAG, "onItemMove: toPosition = " + toPosition); + if (fromPosition == toPosition) { + return; // 直接返回,不执行后续操作 + } + + if (fromPosition < toPosition) { + for (int i = fromPosition; i < toPosition; i++) { + Collections.swap(mContacts, i, i + 1); + } + } else { + for (int i = fromPosition; i > toPosition; i--) { + Collections.swap(mContacts, i, i - 1); + } + } + notifyItemMoved(fromPosition, toPosition); + } + + // 处理滑动删除 + public void onItemDismiss(int position) { +// mContacts.remove(position); + if (mItemMoveCallback != null) { + mItemMoveCallback.onItemRemoved(position); + } + notifyItemRemoved(position); + } + + public class ContactInfoHolder extends RecyclerView.ViewHolder { + NiceImageView nv_avatar; + TextView tv_name, tv_phone; + + public ContactInfoHolder(@NonNull View itemView) { + super(itemView); + nv_avatar = itemView.findViewById(R.id.nv_avatar); + tv_name = itemView.findViewById(R.id.tv_name); + tv_phone = itemView.findViewById(R.id.tv_phone); + } + } +} diff --git a/app/src/main/java/com/ttstd/dialer/contact/AppDatabase.java b/app/src/main/java/com/ttstd/dialer/contact/AppDatabase.java index 6642137..715b771 100644 --- a/app/src/main/java/com/ttstd/dialer/contact/AppDatabase.java +++ b/app/src/main/java/com/ttstd/dialer/contact/AppDatabase.java @@ -5,6 +5,8 @@ import androidx.room.Room; import androidx.room.RoomDatabase; import android.content.Context; +import java.io.File; + @Database(entities = {Contact.class}, version = 1, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { public abstract ContactDao contactDao(); @@ -17,7 +19,7 @@ public abstract class AppDatabase extends RoomDatabase { synchronized (AppDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), - AppDatabase.class, "contact_database") + AppDatabase.class, context.getExternalCacheDir() + File.separator +"contact_database") .allowMainThreadQueries() // 为了简化示例,允许主线程查询 .build(); } diff --git a/app/src/main/java/com/ttstd/dialer/contact/Contact.java b/app/src/main/java/com/ttstd/dialer/contact/Contact.java index 73f2f25..8270db6 100644 --- a/app/src/main/java/com/ttstd/dialer/contact/Contact.java +++ b/app/src/main/java/com/ttstd/dialer/contact/Contact.java @@ -19,10 +19,12 @@ public class Contact implements Serializable { private String name; private String phoneNumber; private String avatar; + private int sort; public Contact(String name, String phoneNumber) { this.name = name; this.phoneNumber = phoneNumber; + this.sort = 0; } public int getId() { @@ -57,6 +59,14 @@ public class Contact implements Serializable { this.avatar = avatar; } + public int getSort() { + return sort; + } + + public void setSort(int sort) { + this.sort = sort; + } + @Override public boolean equals(@Nullable Object obj) { if (obj instanceof Contact) { diff --git a/app/src/main/java/com/ttstd/dialer/contact/ContactDao.java b/app/src/main/java/com/ttstd/dialer/contact/ContactDao.java index 4760e8e..750d84c 100644 --- a/app/src/main/java/com/ttstd/dialer/contact/ContactDao.java +++ b/app/src/main/java/com/ttstd/dialer/contact/ContactDao.java @@ -11,19 +11,19 @@ import java.util.List; @Dao public interface ContactDao { @Insert - void insert(Contact contact); + long insert(Contact contact); @Update - void update(Contact contact); + int update(Contact contact); @Delete - void delete(Contact contact); + int delete(Contact contact); @Query("DELETE FROM contacts WHERE id = :id") - void deleteById(int id); + int deleteById(int id); @Query("DELETE FROM contacts") - void deleteAll(); + int deleteAll(); @Query("SELECT * FROM contacts ORDER BY name ASC") List getAllContacts(); diff --git a/app/src/main/java/com/ttstd/dialer/contact/ContactRepository.java b/app/src/main/java/com/ttstd/dialer/contact/ContactRepository.java index a45e7c8..737e8d4 100644 --- a/app/src/main/java/com/ttstd/dialer/contact/ContactRepository.java +++ b/app/src/main/java/com/ttstd/dialer/contact/ContactRepository.java @@ -30,27 +30,27 @@ public class ContactRepository { } // 添加联系人 - public void insert(Contact contact) { - mContactDao.insert(contact); + public long insert(Contact contact) { + return mContactDao.insert(contact); } // 更新联系人 - public void update(Contact contact) { - mContactDao.update(contact); + public int update(Contact contact) { + return mContactDao.update(contact); } // 删除联系人 - public void delete(Contact contact) { - mContactDao.delete(contact); + public int delete(Contact contact) { + return mContactDao.delete(contact); } // 根据ID删除联系人 - public void deleteById(int id) { - mContactDao.deleteById(id); + public int deleteById(int id) { + return mContactDao.deleteById(id); } // 删除所有联系人 - public void deleteAll() { - mContactDao.deleteAll(); + public int deleteAll() { + return mContactDao.deleteAll(); } } diff --git a/app/src/main/java/com/ttstd/dialer/filter/NoSpaceInputFilter.java b/app/src/main/java/com/ttstd/dialer/filter/NoSpaceInputFilter.java new file mode 100644 index 0000000..b01edc5 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/filter/NoSpaceInputFilter.java @@ -0,0 +1,25 @@ +package com.ttstd.dialer.filter; + +import android.text.InputFilter; +import android.text.Spanned; + +/** + * 过滤输入中的空格(包括普通空格和全角空格) + */ +public class NoSpaceInputFilter implements InputFilter { + @Override + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + // 遍历输入的字符序列,过滤掉所有空格 + StringBuilder filtered = new StringBuilder(); + for (int i = start; i < end; i++) { + char c = source.charAt(i); + // 过滤普通空格(ASCII 32)和全角空格(Unicode 12288) + if (c != ' ' && c != '\u3000') { + filtered.append(c); + } + } + // 如果过滤后的结果与原输入不同,返回过滤后的内容;否则返回原内容(允许输入) + return filtered.length() == (end - start) ? null : filtered; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java index c71ecdf..5265cd2 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java @@ -8,24 +8,20 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; +import android.view.View; import androidx.fragment.app.Fragment; -import android.provider.Settings; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - import com.hjq.toast.Toaster; import com.ttstd.dialer.R; -import com.ttstd.dialer.base.BaseFragment; +import com.ttstd.dialer.activity.contact.list.ContactListActivity; import com.ttstd.dialer.base.mvvm.fragment.BaseMvvmFragment; import com.ttstd.dialer.databinding.FragmentHomeBinding; import com.ttstd.dialer.utils.ApkUtils; import com.ttstd.dialer.utils.DataUtil; import com.ttstd.dialer.utils.LunarCalendarFestivalUtils; -import com.ttstd.dialer.utils.TimeUtils; /** * A simple {@link Fragment} subclass. @@ -190,7 +186,7 @@ public class HomeFragment extends BaseMvvmFragment { + private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator(); + private boolean isAnimatingOut = false; + private boolean visible = true; + + public FabBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, + FloatingActionButton child, + View directTargetChild, + View target, + int axes, + int type) { + return axes == ViewCompat.SCROLL_AXIS_VERTICAL; + } + + @Override + public void onNestedScroll(CoordinatorLayout coordinatorLayout, + FloatingActionButton child, + View target, + int dxConsumed, + int dyConsumed, + int dxUnconsumed, + int dyUnconsumed, + int type) { + super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, type); + + if (dyConsumed > 0 && !isAnimatingOut && child.getVisibility() == View.VISIBLE) { + // 向下滑动,隐藏FAB + animateOut(child); + } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) { + // 向上滑动,显示FAB + animateIn(child); + } + } + + private void animateOut(final FloatingActionButton button) { + if (Build.VERSION.SDK_INT >= 14) { + ViewCompat.animate(button) + .scaleX(0.0f) + .scaleY(0.0f) + .alpha(0.0f) + .setInterpolator(INTERPOLATOR) + .withLayer() + .setListener(new ViewPropertyAnimatorListener() { + public void onAnimationStart(View view) { + isAnimatingOut = true; + } + public void onAnimationCancel(View view) { + isAnimatingOut = false; + } + public void onAnimationEnd(View view) { + isAnimatingOut = false; + view.setVisibility(View.INVISIBLE); + } + }).start(); + } else { + Animation anim = AnimationUtils.loadAnimation(button.getContext(), R.anim.fab_out); + anim.setInterpolator(INTERPOLATOR); + anim.setDuration(200L); + anim.setAnimationListener(new Animation.AnimationListener() { + public void onAnimationStart(Animation animation) { + isAnimatingOut = true; + } + public void onAnimationEnd(Animation animation) { + isAnimatingOut = false; + button.setVisibility(View.INVISIBLE); + } + @Override + public void onAnimationRepeat(final Animation animation) {} + }); + button.startAnimation(anim); + } + } + + private void animateIn(FloatingActionButton button) { + button.setVisibility(View.VISIBLE); + if (Build.VERSION.SDK_INT >= 14) { + ViewCompat.animate(button) + .scaleX(1.0f) + .scaleY(1.0f) + .alpha(1.0f) + .setInterpolator(INTERPOLATOR) + .withLayer() + .setListener(null) + .start(); + } else { + Animation anim = AnimationUtils.loadAnimation(button.getContext(), R.anim.fab_in); + anim.setDuration(200L); + anim.setInterpolator(INTERPOLATOR); + button.startAnimation(anim); + } + } +} diff --git a/app/src/main/java/com/ttstd/dialer/view/ItemTouchHelperCallback.java b/app/src/main/java/com/ttstd/dialer/view/ItemTouchHelperCallback.java new file mode 100644 index 0000000..d3433a6 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/view/ItemTouchHelperCallback.java @@ -0,0 +1,54 @@ +package com.ttstd.dialer.view; + +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.ttstd.dialer.adapter.ContactInfoAdapter; + + +// 处理拖动和滑动事件 +public class ItemTouchHelperCallback extends ItemTouchHelper.Callback { + private final ContactInfoAdapter mAdapter; + + public ItemTouchHelperCallback(ContactInfoAdapter adapter) { + mAdapter = adapter; + } + + // 定义支持的拖动方向 + @Override + public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + // 允许上下拖动 + int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN ; + // 允许左右滑动删除 + int swipeFlags = ItemTouchHelper.START; + return makeMovementFlags(dragFlags, swipeFlags); + } + + // 处理拖动交换位置 + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, + RecyclerView.ViewHolder target) { + // 通知适配器项已移动 + mAdapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition()); + return true; + } + + // 处理滑动删除 + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + // 通知适配器项已删除 + mAdapter.onItemDismiss(viewHolder.getAdapterPosition()); + } + + // 当长按item时启用拖动 + @Override + public boolean isLongPressDragEnabled() { + return true; + } + + // 启用滑动删除 + @Override + public boolean isItemViewSwipeEnabled() { + return true; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/view/ScrollAwareFABBehavior.java b/app/src/main/java/com/ttstd/dialer/view/ScrollAwareFABBehavior.java new file mode 100644 index 0000000..6c718ef --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/view/ScrollAwareFABBehavior.java @@ -0,0 +1,87 @@ +package com.ttstd.dialer.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.ViewCompat; +import androidx.core.view.ViewPropertyAnimatorListener; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + + +public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior { + private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator(); + private boolean mIsAnimatingOut = false; + + public ScrollAwareFABBehavior(Context context, AttributeSet attrs) { + super(); + } + + @Override + public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child, + final View directTargetChild, final View target, final int nestedScrollAxes) { + // Ensure we react to vertical scrolling + return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL + || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes); + } + + @Override + public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child, + final View target, final int dxConsumed, final int dyConsumed, + final int dxUnconsumed, final int dyUnconsumed) { + super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); + if (dyConsumed > 0 && !this.mIsAnimatingOut && child.getVisibility() == View.VISIBLE) { + // User scrolled down and the FAB is currently visible -> hide the FAB + animateOut(child); + } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) { + // User scrolled up and the FAB is currently not visible -> show the FAB + animateIn(child); + } + } + + private void animateOut(final FloatingActionButton button) { + ViewCompat.animate(button).translationY(button.getHeight() + getMarginBottom(button)).setInterpolator(INTERPOLATOR).withLayer() + .setListener(new ViewPropertyAnimatorListener() { + @Override + public void onAnimationStart(View view) { + ScrollAwareFABBehavior.this.mIsAnimatingOut = true; + } + + @Override + public void onAnimationCancel(View view) { + ScrollAwareFABBehavior.this.mIsAnimatingOut = false; + } + + @Override + public void onAnimationEnd(View view) { + ScrollAwareFABBehavior.this.mIsAnimatingOut = false; + view.setVisibility(View.INVISIBLE); + } + }).start(); + } + + private void animateIn(FloatingActionButton button) { + button.setVisibility(View.VISIBLE); + ViewCompat.animate(button).translationY(0) + .setInterpolator(INTERPOLATOR).withLayer().setListener(null) + .start(); + } + + private int getMarginBottom(View v) { + int marginBottom = 0; + final ViewGroup.LayoutParams layoutParams = v.getLayoutParams(); + if (layoutParams instanceof ViewGroup.MarginLayoutParams) { + marginBottom = ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin; + } + return marginBottom; + } +} diff --git a/app/src/main/res/anim/fab_in.xml b/app/src/main/res/anim/fab_in.xml new file mode 100644 index 0000000..dcd9d9d --- /dev/null +++ b/app/src/main/res/anim/fab_in.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_out.xml b/app/src/main/res/anim/fab_out.xml new file mode 100644 index 0000000..dcd9d9d --- /dev/null +++ b/app/src/main/res/anim/fab_out.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_default_avatar.xml b/app/src/main/res/drawable/ic_default_avatar.xml new file mode 100644 index 0000000..7a09e7b --- /dev/null +++ b/app/src/main/res/drawable/ic_default_avatar.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/activity_contact_add.xml b/app/src/main/res/layout/activity_contact_add.xml new file mode 100644 index 0000000..b80fb1a --- /dev/null +++ b/app/src/main/res/layout/activity_contact_add.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_contact_edit.xml b/app/src/main/res/layout/activity_contact_edit.xml new file mode 100644 index 0000000..fdc4c77 --- /dev/null +++ b/app/src/main/res/layout/activity_contact_edit.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_contact_list.xml b/app/src/main/res/layout/activity_contact_list.xml new file mode 100644 index 0000000..2e20e1c --- /dev/null +++ b/app/src/main/res/layout/activity_contact_list.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_contact.xml b/app/src/main/res/layout/item_contact.xml new file mode 100644 index 0000000..5befc3d --- /dev/null +++ b/app/src/main/res/layout/item_contact.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fe24b8d..b0ae1f3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,4 +3,5 @@ Hello blank fragment + com.ttstd.dialer.view.ScrollAwareFABBehavior