实现fab随RecyclerView滑动隐藏显示,增加添加联系人页面,实现联系人拖动

This commit is contained in:
2025-10-22 00:21:20 +08:00
parent 68b2e0754c
commit 80f7e47511
25 changed files with 982 additions and 25 deletions

View File

@@ -25,6 +25,18 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activity.contact.add.ContactAddActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<activity
android:name=".activity.contact.edit.ContactEditActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<activity
android:name=".activity.contact.list.ContactListActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<provider

View File

@@ -0,0 +1,65 @@
package com.ttstd.dialer.activity.contact.add;
import android.text.InputFilter;
import android.view.View;
import androidx.lifecycle.Observer;
import com.ttstd.dialer.R;
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
import com.ttstd.dialer.contact.Contact;
import com.ttstd.dialer.databinding.ActivityContactAddBinding;
import com.ttstd.dialer.filter.NoSpaceInputFilter;
public class ContactAddActivity extends BaseMvvmActivity<ContactAddViewModel, ActivityContactAddBinding> {
@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<Long>() {
@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);
}
}
}

View File

@@ -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<ActivityContactAddBinding, ActivityEvent> {
private static final String TAG = "ContactAddViewModel";
private ContactRepository mRepository;
@Override
public void setContext(Context context) {
super.setContext(context);
mRepository = new ContactRepository(context);
}
public MutableLiveData<Long> mIntegerMutableLiveData = new MutableLiveData<>();
public void insert(Contact contact) {
Log.e(TAG, "insert: " );
mIntegerMutableLiveData.setValue(mRepository.insert(contact));
}
}

View File

@@ -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<ContactEditViewModel, ActivityContactEditBinding> {
@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{
}
}

View File

@@ -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 <ActivityContactEditBinding, ActivityEvent>{
}

View File

@@ -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<ContactListViewModel, ActivityContactListBinding> {
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<List<Contact>>() {
@Override
public void onChanged(List<Contact> 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));
}
}
}

View File

@@ -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<ActivityContactListBinding, ActivityEvent> {
private static final String TAG = "ContactListViewModel";
private ContactRepository mRepository;
@Override
public void setContext(Context context) {
super.setContext(context);
mRepository = new ContactRepository(context);
}
public MutableLiveData<List<Contact>> mContactListData = new MutableLiveData<>();
public void getAllContacts() {
List<Contact> contacts = mRepository.getAllContacts();
List<Contact> sorted = contacts.stream().sorted(new Comparator<Contact>() {
@Override
public int compare(Contact o1, Contact o2) {
return Integer.compare(o1.getSort(), o2.getSort());
}
}).collect(Collectors.toList());
mContactListData.setValue(sorted);
}
public List<Contact> searchContacts(String query) {
return mRepository.searchContacts(query);
}
}

View File

@@ -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<ContactInfoAdapter.ContactInfoHolder> {
private static final String TAG = "ContactInfoAdapter";
private FragmentActivity mContext;
private List<Contact> mContacts;
public void setContacts(List<Contact> 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);
}
}
}

View File

@@ -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();
}

View File

@@ -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) {

View File

@@ -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<Contact> getAllContacts();

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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<HomeViewModel, FragmentHomeBi
public class BtnClick {
public void openContact(View view) {
Intent intent = new Intent();
Intent intent = new Intent(mContext, ContactListActivity.class);
try {
startActivity(intent);
} catch (Exception e) {

View File

@@ -0,0 +1,117 @@
package com.ttstd.dialer.view;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior;
import androidx.core.view.ViewCompat;
import androidx.core.view.ViewPropertyAnimatorListener;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.ttstd.dialer.R;
public class FabBehavior extends Behavior<FloatingActionButton> {
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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:fromAlpha="0.0"
android:toAlpha="1.0" />

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:fromAlpha="0.0"
android:toAlpha="1.0" />

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M0,0m91.02,0l841.96,0q91.02,0 91.02,91.02l0,841.96q0,91.02 -91.02,91.02l-841.96,0q-91.02,0 -91.02,-91.02l0,-841.96q0,-91.02 91.02,-91.02Z"
android:fillColor="#C5C5C5"/>
<path
android:pathData="M794.83,824.43c-60.46,-27.12 -37.16,-6.35 -111.37,-34.16 -74.18,-27.67 -91.75,-36.75 -91.75,-36.75l4.3,-48.51s68.12,-21.17 80.9,-104.25c25.62,6.28 34.29,-25.28 35.66,-45.51 1.58,-19.54 15.18,-80.33 -16.19,-74.93 6.43,-40.58 11.49,-77.28 9.16,-96.75 -7.79,-68.19 -63.67,-139.38 -204.62,-144.63 -31.79,1.17 -63.15,7.76 -92.71,19.47 -10.39,4.1 -77.89,-4.3 -86.97,14.14 -4.93,10.05 10.24,40.52 2.05,47.08 -33,26.51 -24.12,31.22 -27.88,64.01 -2.25,19.47 2.32,56.09 8.74,96.81 -31.29,-5.46 -17.75,55.33 -16.38,74.87 1.42,20.23 9.9,51.93 35.59,45.65C336.13,684.04 419.41,705.42 419.41,705.42l4.31,48.72s-17.57,9.69 -91.76,37.43c-74.18,27.74 -50.89,5.73 -111.35,32.79C125.16,867.41 125.16,985.33 125.16,985.33c0,17.91 16.05,32.46 35.86,32.46H854.47c19.82,0 35.87,-14.56 35.87,-32.39v-0.07s0,-117.92 -95.52,-160.82v-0.07z"
android:fillColor="#E5E5E5"/>
</vector>

View File

@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.contact.add.ContactAddActivity">
<data>
<variable
name="click"
type="com.ttstd.dialer.activity.contact.add.ContactAddActivity.BtnClick" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/tv_save"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="48dp">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:maxLines="1"
android:singleLine="true"
android:text="姓名"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="姓名"
android:maxLines="1"
android:singleLine="true"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/tv_name"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="48dp">
<TextView
android:id="@+id/tv_phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:maxLines="1"
android:singleLine="true"
android:text="电话"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_phone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="电话"
android:inputType="phone"
android:maxLines="1"
android:singleLine="true"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/tv_phone"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<TextView
android:id="@+id/tv_save"
android:layout_width="wrap_content"
android:onClick="@{click::save}"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="32dp"
android:maxLines="1"
android:singleLine="true"
android:text="保存"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.contact.edit.ContactEditActivity">
<data>
<variable
name="click"
type="com.ttstd.dialer.activity.contact.edit.ContactEditActivity.BtnClick" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.contact.list.ContactListActivity">
<data>
<variable
name="click"
type="com.ttstd.dialer.activity.contact.list.ContactListActivity.BtnClick" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="32dp"
android:layout_marginBottom="32dp"
android:onClick="@{click::addContact}"
android:src="@drawable/ic_add"
app:fabCustomSize="64dp"
app:layout_behavior="com.ttstd.dialer.view.ScrollAwareFABBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="100dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.shehuan.niv.NiceImageView
android:id="@+id/nv_avatar"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_default_avatar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/nv_avatar"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:singleLine="true"
android:text="姓名"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:maxLines="1"
android:singleLine="true"
android:text="姓名"
android:textColor="@color/gray"
android:textSize="13sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -3,4 +3,5 @@
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="floating_action_button_behavior">com.ttstd.dialer.view.ScrollAwareFABBehavior</string>
</resources>