Android高仿陌陌应用点点滑动效果

news2025/7/21 5:23:23

效果图:

分析:

从效果上看图片的展示具有层次感,在数据结构上更像是stack,所以通过继承FrameLayout来实现(不清楚FrameLayout布局特点的可以先百度下哈),外面是通过继承FrameLayout自定义的TinderStackLayout,它主要作为一个容器,里面的卡片效果也是继承FrameLayout自定义TinderCardView,需要对其onTouch()事件进行监听,设置阀值来判断TinderCardView的移动距离,然后通过设置动画即可实现。

具体实现:

TinderCardView实现:

1、在布局上最外层使用CardView,里面包含两部分内容,上面ImageView,下面TextView.通过inflate设置内容视图。在最顶层还需要一个ImageView来作为Tips

2、通过变量downX,downY来记录按下时的坐标位置。

3、在移动过程中dX,dY来记录视图移动的坐标,然后重新设置移动后的视图位置,同时还要计算和设置视图的旋转角度以及tips的透明度等。

4、当手指抬起的时候需要判断视图向左或者向右移动的距离是否超过设定的阀值,如果超过了就从父容器TinderCardLayout中移除,否则重置视图位置。

5、在移除视图之后需要判断父容器内子视图的数量,如果仅剩下一个子视图,就通知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实现:

作为父容器,通过addView()方法添加子视图,设置变量STACK_SIZE来表示父容器内可以展示的子视图数量。

在添加子视图时需要计算和设置子视图的缩放以及显示位置(这样才能达到层级显示效果)

onLoad()方法作为数据的回调,当父容器里面只有一个子视图时,就会调用该方法去加载数据。

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工具类:

主要是获取设备的屏幕宽度以及dip与px之间的转化(计算偏移量和缩放比例需要)。

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实体类:

具体的数据模型,封装需要展示的基本信息。

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中的数据来源来自于values目录下的arrays.xml(这里就不再具体描述,详见源码),然后把数据传递给TinderStackLayout进行管理

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

源码地址:

https: //github.com/Mersens/TinderStackView

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/368613.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-基础篇

SpringBoot基础篇 ​ 在基础篇中&#xff0c;我给学习者的定位是先上手&#xff0c;能够使用SpringBoot搭建基于SpringBoot的web项目开发&#xff0c;所以内容设置较少&#xff0c;主要包含如下内容&#xff1a; SpringBoot快速入门SpringBoot基础配置基于SpringBoot整合SSMP…

为你的Vue2.x老项目安装Vite发动机吧

天下苦webpack久矣&#xff0c;相信作为前端开发者一定经历过在项目迭代时间较长的时候经历漫长等待的这一过程&#xff0c;每一次保存都会浪费掉大量时间&#xff0c;这是webpack这种机制所带来的问题。 于是&#xff0c;尤大为我们带来了新一代前端构建工具&#xff1a;vite…

搜索旋转排序数组、路径总和 II、拆分数字

文章目录搜索旋转排序数组&#xff08;数组、二分查找&#xff09;路径总和 II&#xff08;树、深度优先搜索&#xff09;拆分数字&#xff08;算法&#xff09;搜索旋转排序数组&#xff08;数组、二分查找&#xff09; 整数数组 nums 按升序排列&#xff0c;数组中的值 互不…

Transformer学习笔记

Transformer学习笔记1. 参考2. 模型图3.encoder部分3.1 Positional Encoding3.2 Muti-Head Attention3.3 ADD--残差连接3.4 Norm标准化3.5 单个Transformer Encoder流程图4.decoder部分4.1 mask Muti-Head Attention4.2 Muti-Head Attention5 多个Transformer Encoder和多个Tra…

详解数据库基本概念

数据库&#xff08;DataBase 简称 DB&#xff09;&#xff1a;是一个长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合数据库管理系统&#xff08;DataBase Management System 简称 DBMS&#xff09;&#xff1a;是一种操纵和管理数据库的大型软件&#xf…

基于嵌入式linux的DHCP服务器的搭建与移植(udhcp)

DHCP是Dynamic Host Configuration Protocol的缩写&#xff0c;即动态主机配置协议。DHCP是一个很重要的局域网的网络协议&#xff0c;使用UDP协议实现动态配置功能&#xff0c;主要有以下用途&#xff1a; 1、为内部网络或网络服务供应商自动分配IP地址&#xff1b; 2、为用…

利用Python和Sprak求曲线与X轴上方的面积

有n组标本(1, 2, 3, 4), 每组由m个( , , ...)元素( , )组成(m值不定), . 各组样本的分布 曲线如下图所示. 通过程序近似实现各曲线与oc, cd直线围成的⾯积. 思路 可以将图像分成若干个梯形&#xff0c;每个梯形的底边长为(Xn1 - Xn-1)&#xff0c;面积为矩形的一半&#xff0c…

原创壁纸小程序独立后台(1.3.5版本介绍)

1、新版本开发目的 历经前两次版本迭代&#xff0c;本人发现在整个系统的架构方面存在一定的缺陷&#xff0c;这种缺陷就是前后端不分离&#xff0c;导致在后期的维护方面遇到了很多问题。 那么这次版本更新并没有带来很多新的功能&#xff0c;而是重构了系统并优化UI&#x…

【PyQt5图形界面编程(2)】:创建工程

创建工程 一、创建工程二、开始开发1、运行Qt5Designer,创建QT窗口2、运行pyUIC,转换xx.ui成xx.py3、main.py中引用xx.py中的类4、打包main.py成main.exe来发布5、执行终端报警处理方法三、其他(如果涉及)1、配置环境变量一、创建工程 采用虚拟环境来创建工程 相关的paka…

STM32FreeRTOS - 按键实现任务挂起和恢复

STM32f103C8T6 FreeRTOS - 按键实现任务挂起和恢复&#xff0c;按键按下时&#xff0c;LED任务执行&#xff0c;led闪烁&#xff0c;当led任务挂起&#xff0c;Led停止闪烁。1.STM32CubeMX 创建任务1.1配置GPIO按键配置外部中断触发GPIO绿灯&#xff0c;红灯配置输出模式1.2配置…

Android中级——色彩处理和图像处理

色彩处理 通过色彩矩阵处理 色彩矩阵介绍 图像的RGBA可拆分为一个4行5列的矩阵和5行1列矩阵相乘 其中4行5列矩阵即为ColorMatrix&#xff0c;可通过调整ColorMatrix间接调整RGBA 第一行 abcde 决定新的 R第二行 fghij 决定新的 G第三行 klmno 决定新的 G第四行 pqrst 决定新…

Ubuntu64位下安装Anaconda3的详细过程

Ubuntu下安装Anaconda的详细过程 下载 Anaconda&#xff1a;首先&#xff0c;您需要在 Anaconda 的官网上下载适合您的 Linux 版本的安装包。您可以使用以下命令下载最新版本&#xff1a; wget https://repo.anaconda.com/archive/Anaconda3-2021.05-Linux-x86_64.sh下载完毕后…

春季 3 月 · CSM 认证周末班【提前报名特惠】“全球金牌课程”CST 导师亲授

为什么“模块化分时段”单元教学 ☆ 有脑科学研究资料揭示: 成人学习者持续 3.5 小时已经达到极限&#xff0c;新模式教学&#xff0c;给学习者留有一些时间和空间去消化吸收&#xff0c;反思回顾(Reflection)&#xff0c;讲师布置一些小作业&#xff0c;让学员课后去练习&…

React Draggable 实现图片拖拽

React Draggable 实现拖拽 React Draggable 是 react 生态中&#xff0c;最好用的拖拽实现库之一。如果你的应用中需要实现拖拽功能&#xff0c;可以尝试用 react-draggable&#xff0c;它可以满足多数情况下的拖拽需求&#xff0c;比如一个弹出设置浮窗&#xff0c;可以相互遮…

2月更新!EasyOps又迎来新升级,解锁9大新特性

又到了每月产品盘点时刻&#xff0c;9大新功能上线和升级优化&#xff0c;涉及Hyperlnsight超融合持续观测平台、CMDB立体化资源管理平台、DevOps持续交付平台、AutoOps自动化运维平台、EasyHub资源共享平台&#xff0c;在不断的技术创新过程中&#xff0c;进一步加速IT运维效率…

Android framework socketpair

简述 在Linux中&#xff0c;socketpair函数可以用于创建一对相互连接的、通信域为AF_UNIX的套接字&#xff0c;其中一个套接字可用于读取&#xff0c;另一个套接字可用于写入。可以使用这对套接字在同一进程内进行进程间通信&#xff08;IPC&#xff09;。 以下是使用socketp…

python 学习

1.转义字符 字符串前加r&#xff0c;使转义字符不起作用 print(r"hello\t world") 2.数据类型 python数据类型分为int,float,bool,str 整数类型的形式: print("二进制0b开头", 0b10111001) print("八进制0o开头", 0o1257436) print("…

Unable to connect to Redis无法连接到Redis

文章目录项目场景&#xff1a;问题描述原因分析&#xff1a;解决方案&#xff1a;项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 在某个项目中的提交按钮不好用 org.springframework.data.redis.RedisConnectionFailureException: Unable to con…

【线程基础篇】

进程 进程本质是一个正在执行的程序。程序运行时&#xff0c;系统会创建进程&#xff0c;并给进程分配独立的内存空间。 CPU时间片切换 CPU时间片切换时&#xff0c;进程需要从切换之前运行的位置开始执行&#xff0c;所以进程还包括程序计数器、堆栈指针。 单核CPU时&…

如何拼接字符串-探究“+号拼接”/“StringBuilder”在不同场景下的效率问题

拼接字符串&#xff0c;在程序开发中很常见也很常用&#xff0c;大家都会&#xff1a;号拼接String.concat(String str)StringBuffer / StringBuilder但这里今天主要探究“号拼接”&#xff0c;“StringBuilder”在不同场景下的效率问题。不都说 StringBuilder 在处理字符串拼接…