寫在前面的話
寫代碼一直不太喜歡加注解彼哼,但是代碼的思路很清晰巍实,接下來會對具體實現(xiàn)簡單講解
廢話不說,先上效果圖:
分析
從效果上看圖片的展示具有層次感姜贡,在數(shù)據(jù)結(jié)構(gòu)上更像是stack,所以通過繼承FrameLayout來實現(xiàn)(不清楚FrameLayout布局特點(diǎn)的可以先百度下哈)撩穿,外面是通過繼承FrameLayout自定義的TinderStackLayout磷支,它主要作為一個容器,里面的卡片效果也是繼承FrameLayout自定義TinderCardView食寡,需要對其onTouch()事件進(jìn)行監(jiān)聽雾狈,設(shè)置閥值來判斷TinderCardView的移動距離,然后通過設(shè)置動畫即可實現(xiàn)抵皱。
具體實現(xiàn)
TinderCardView實現(xiàn)
1:在布局上最外層使用CardView,里面包含兩部分內(nèi)容善榛,上面ImageView,下面TextView.通過inflate設(shè)置內(nèi)容視圖。在最頂層還需要一個ImageView來作為Tips
2:通過變量downX叨叙,downY來記錄按下時的坐標(biāo)位置锭弊。
3:在移動過程中dX,dY來記錄視圖移動的坐標(biāo)堪澎,然后重新設(shè)置移動后的視圖位置擂错,同時還要計算和設(shè)置視圖的旋轉(zhuǎn)角度以及tips的透明度等。
4:當(dāng)手指抬起的時候需要判斷視圖向左或者向右移動的距離是否超過設(shè)定的閥值樱蛤,如果超過了就從父容器TinderCardLayout中移除钮呀,否則重置視圖位置剑鞍。
5:在移除視圖之后需要判斷父容器內(nèi)子視圖的數(shù)量,如果僅剩下一個子視圖爽醋,就通知TinderCardLayout加載更多蚁署。
public class TinderCardView extends FrameLayout implements View.OnTouchListener {
private static final int PADDINGVALUE=16;
private static final float CARD_ROTATION_DEGREES = 40.0f;
public static final int DURATIONTIME=300;
private ImageView iv;
private TextView tv_name;
private ImageView iv_tips;
private int padding;
private float downX;
private float downY;
private float newX;
private float newY;
private float dX;
private float dY;
private float rightBoundary;
private float leftBoundary;
private int screenWidth;
private OnLoadMoreListener listener;
public TinderCardView(Context context) {
this(context,null);
}
public TinderCardView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public TinderCardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public void init(Context context){
if(!isInEditMode()){
inflate(context,R.layout.cardview,this);
screenWidth=DensityUtil.getScreenWidth(context);
leftBoundary = screenWidth * (1.0f/6.0f);
rightBoundary = screenWidth * (5.0f/6.0f);
iv=(ImageView) findViewById(R.id.iv);
tv_name=(TextView) findViewById(R.id.tv_name);
iv_tips=(ImageView)findViewById(R.id.iv_tips);
padding = DensityUtil.dip2px(context, PADDINGVALUE);
setOnTouchListener(this);
}
}
@Override
public boolean onTouch(final View view, MotionEvent motionEvent) {
TinderStackLayout tinderStackLayout = ((TinderStackLayout) view.getParent());
TinderCardView topCard = (TinderCardView) tinderStackLayout.getChildAt(tinderStackLayout.getChildCount() - 1);
if (topCard.equals(view)) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = motionEvent.getX();
downY = motionEvent.getY();
view.clearAnimation();
return true;
case MotionEvent.ACTION_MOVE:
newX = motionEvent.getX();
newY = motionEvent.getY();
dX = newX - downX;
dY = newY - downY;
float posX = view.getX() + dX;
view.setX(view.getX() + dX);
view.setY(view.getY() + dY);
float rotation = (CARD_ROTATION_DEGREES * (posX)) / screenWidth;
int halfCardHeight = (view.getHeight() / 2);
if(downY < halfCardHeight - (2*padding)){
view.setRotation(rotation);
} else {
view.setRotation(-rotation);
}
float alpha = (posX - padding) / (screenWidth * 0.3f);
if(alpha>0){
iv_tips.setAlpha(alpha);
iv_tips.setImageResource(R.drawable.ic_like);
}else{
iv_tips.setAlpha(-alpha);
iv_tips.setImageResource(R.drawable.ic_nope);
}
return true;
case MotionEvent.ACTION_UP:
if(isBeyondLeftBoundary(view)){
removeCard(view, -(screenWidth * 2));
}
else if(isBeyondRightBoundary(view)){
removeCard(view,(screenWidth * 2));
}else{
resetCard(view);
}
return true;
default :
return super.onTouchEvent(motionEvent);
}
}
return super.onTouchEvent(motionEvent);
}
private boolean isBeyondLeftBoundary(View view){
return (view.getX() + (view.getWidth() / 2) < leftBoundary);
}
private boolean isBeyondRightBoundary(View view){
return (view.getX() + (view.getWidth() / 2) > rightBoundary);
}
private void removeCard(final View view, int xPos){
view.animate()
.x(xPos)
.y(0)
.setInterpolator(new AccelerateInterpolator())
.setDuration(DURATIONTIME)
.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
ViewGroup viewGroup = (ViewGroup) view.getParent();
if(viewGroup != null) {
viewGroup.removeView(view);
}
int count=viewGroup.getChildCount();
if(count==1 && listener!=null){
listener.onLoad();
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
}
private void resetCard(final View view){
view.animate()
.x(0)
.y(0)
.rotation(0)
.setInterpolator(new OvershootInterpolator())
.setDuration(DURATIONTIME);
iv_tips.setAlpha(0f);
}
public void bind(User u){
if(u==null){
return;
}
if(!TextUtils.isEmpty(u.getAvatarUrl())){
Glide.with(iv.getContext())
.load(u.getAvatarUrl())
.into(iv);
}
if(!TextUtils.isEmpty(u.getName())){
tv_name.setText(u.getName());
}
}
public interface OnLoadMoreListener{
void onLoad();
}
public void setOnLoadMoreListener(OnLoadMoreListener listener){
this.listener=listener;
}
}
TinderStackLayout實現(xiàn)
作為父容器,通過addView()方法添加子視圖蚂四,設(shè)置變量STACK_SIZE來表示父容器內(nèi)可以展示的子視圖數(shù)量光戈。
在添加子視圖時需要計算和設(shè)置子視圖的縮放以及顯示位置(這樣才能達(dá)到層級顯示效果)
onLoad()方法作為數(shù)據(jù)的回調(diào),當(dāng)父容器里面只有一個子視圖時遂赠,就會調(diào)用該方法去加載數(shù)據(jù)久妆。
public class TinderStackLayout extends FrameLayout implements TinderCardView.OnLoadMoreListener {
private ViewGroup.LayoutParams params = null;
public static final float BASESCALE_X_VALUE = 50.0f;
public static final int BASESCALE_Y_VALUE = 8;
public static final int DURATIONTIME = 300;
private static final int STACK_SIZE = 4;
private int index = 0;
private int scaleY;
private TinderCardView tc;
private List<User> mList;
public TinderStackLayout(Context context) {
this(context, null);
}
public TinderStackLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TinderStackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public void init() {
params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
scaleY = DensityUtil.dip2px(getContext(), BASESCALE_Y_VALUE);
}
private void addCard(TinderCardView view) {
int count = getChildCount();
addView(view, 0, params);
float scaleX = 1 - (count / BASESCALE_X_VALUE);
view.animate()
.x(0)
.y(count * scaleY)
.scaleX(scaleX)
.setInterpolator(new AnticipateOvershootInterpolator())
.setDuration(DURATIONTIME);
}
public void setDatas(List<User> list) {
this.mList = list;
if (mList == null) {
return;
}
for (int i = index; index < i + STACK_SIZE; index++) {
tc = new TinderCardView(getContext());
tc.bind(mList.get(index));
tc.setOnLoadMoreListener(this);
addCard(tc);
}
}
@Override
public void onLoad() {
for (int i = index; index < i + (STACK_SIZE - 1); index++) {
if (index == mList.size()) {
return;
}
tc = new TinderCardView(getContext());
tc.bind(mList.get(index));
tc.setOnLoadMoreListener(this);
addCard(tc);
}
int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
TinderCardView tinderCardView = (TinderCardView) getChildAt(i);
if (tinderCardView != null) {
float scaleValue = 1 - ((childCount - 1 - i) / 50.0f);
tinderCardView.animate()
.x(0)
.y((childCount - 1 - i) * scaleY)
.scaleX(scaleValue)
.rotation(0)
.setInterpolator(new AnticipateOvershootInterpolator())
.setDuration(DURATIONTIME);
}
}
}
}
DensityUtil工具類
主要是獲取設(shè)備的屏幕寬度以及dip與px之間的轉(zhuǎn)化(計算偏移量和縮放比例需要)。
public class DensityUtil {
public static int getScreenWidth(Context context){
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
return wm.getDefaultDisplay().getWidth();
}
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
}
User實體類
具體的數(shù)據(jù)模型跷睦,封裝需要展示的基本信息筷弦。
private String name;
private String avatarUrl;
public User(String name, String avatarUrl) {
this.name = name;
this.avatarUrl = avatarUrl;
}
public User() {
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
cardview.xml
<android.support.v7.widget.CardView 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"
android:layout_margin="36dp"
app:cardCornerRadius="5dp"
android:layout_gravity="top">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv"
android:layout_width="match_parent"
android:layout_height="320dp"
android:layout_marginBottom="5dp"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_vertical"
android:padding="16dp"
android:background="@android:color/white"
android:layout_gravity="bottom">
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:text="Mersens"
android:textSize="22sp"/>
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="top"
android:clipChildren="true">
<ImageView
android:id="@+id/iv_tips"
android:layout_height="62dp"
android:layout_width="62dp"
android:src="@drawable/ic_like"
android:layout_gravity="center"
android:layout_marginBottom="50dp"
android:alpha="0"
/>
</FrameLayout>
</FrameLayout>
</android.support.v7.widget.CardView>
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.mersens.tinderstackview.activity.MainActivity">
<com.mersens.tinderstackview.view.TinderStackLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/tinderStackLayout"
/>
</LinearLayout>
MainActivity
在MainActivity中的數(shù)據(jù)來源來自于values目錄下的arrays.xml(這里就不再具體描述,詳見源碼)抑诸,然后把數(shù)據(jù)傳遞給TinderStackLayout進(jìn)行管理
public class MainActivity extends AppCompatActivity {
private TinderStackLayout tinderStackLayout;
private List<User> mList;
private String names[];
private String avatarUrls[];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
public void init(){
mList=new ArrayList<User>();
names=getResources().getStringArray(R.array.names);
avatarUrls=getResources().getStringArray(R.array.avatar_urls);
tinderStackLayout=(TinderStackLayout)findViewById(R.id.tinderStackLayout);
for(int i=0;i<names.length;i++){
User user=new User(names[i],avatarUrls[i]);
mList.add(user);
}
tinderStackLayout.setDatas(mList);
}
}