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