云村来了,各位客官这么看?

战略

关系 + 内容社区

核心

消费 + 互动 + 关系缔结

热评墙

精选的Mlog

mlog

类似于Vlog一样,前者是用视频记录自己的生活。
后者是用音乐记录自己的生活

project001

视频展示:

屏幕录制2019-12-17下午6.12.22.mov (25.84MB)

源文件:

云村.framerx.zip

交互动效分析

搜索栏:

一开始的话,搜索栏是不出现的。随着用户向上滑动,当往下滑动的时候,搜索栏才会出现。
我估计可能云村这里,交互设计师是想让用户先浏览大数据推荐的或者是优选的内容,一般当用户向下滑动的时候,有可能会搜索内容,所以才这样设计的。

发图文和视频的小按钮

这里的按钮,处在大拇指扫射的范围内,正好是向左的极限距离(右手单手操作的情况下)。
我估计啊,应该是右手操作比左手操作的情况多,所以放在左下的位置有两个。

  • 方便右手操作,按钮的位置处在右手大拇指可触及的范围内
  • 放在左变,是怕右手误触。
  • 一开始按钮会有一个微动效,这可能也是官方为了鼓励用户多发发自己的动态。

内容区

内容区我大概看了一下,主要有三类。

  • 视频模块
  • 图文模块
  • 音频图文模块

project002

今日份更新:发文小按钮动效

demo.mov (1.46MB)

demo源文件:

发文小按钮demo.framerx.zip

总文件源文件:

云村1.01.framerx.zip

101.mov (5.37MB)

案例分析:

这个小动效是首次跳转到云村这个导航时,会触发一次。
当点击返回的时候,会触发一次。
意在引导用户发文或者发视频。

潜在思考

为什么他这里不可以设置为自动能拖动小按钮的位置呢?
克制 与 自由 ?

技巧分析

主要用了 useAnimation ,组件间交互比data更为灵活,尤其是在处理复杂的动画的时候。

代码展示:

  1. import { Override, useAnimation } from "framer"
  2. export function Tap(): Override {
  3. return {
  4. onTap() {
  5. ButtonCircleControls.start({
  6. rotate: 360,
  7. transition: {
  8. duration: 1,
  9. loop: 1,
  10. repeatDelay: 1.5,
  11. },
  12. })
  13. ButtonPlusControls.start({
  14. scale: 0,
  15. opacity: 0,
  16. transition: {
  17. yoyo: 1,
  18. duration: 1,
  19. repeatDelay: 1.5,
  20. ease:"easeInOut"
  21. },
  22. })
  23. ButtonMControls.start({
  24. scale: 1,
  25. opacity: 1,
  26. transition: {
  27. delay:0.3,
  28. yoyo: 1,
  29. duration: 1,
  30. repeatDelay: 1,
  31. ease:"easeInOut"
  32. },
  33. })
  34. },
  35. }
  36. }
  37. //buttonCircle
  38. let ButtonCircleControls
  39. export function ButtonCircle(): Override {
  40. ButtonCircleControls = useAnimation()
  41. return {
  42. animate: ButtonCircleControls,
  43. }
  44. }
  45. //button+
  46. let ButtonPlusControls
  47. export function ButtonPlus(): Override {
  48. ButtonPlusControls = useAnimation()
  49. return {
  50. animate: ButtonPlusControls,
  51. }
  52. }
  53. //buttonM
  54. let ButtonMControls
  55. export function ButtonM(): Override {
  56. ButtonMControls = useAnimation()
  57. return {
  58. scale: 0,
  59. animate: ButtonMControls,
  60. }
  61. }

project003

本次更新:表白墙滚动效果

3.mov (3.56MB)

demo源文件

热评墙.framerx.zip

案例分析

这个热评墙的热评滚动效果是用户首次点到云村这里的时候,会触发的。
一开始是一个人性化的打动你的文案。然后接下来是三个热评的文案,再然后就还是那个文案。
关于这个热评墙,既然是网易云做的,那应该就和音频有关系。果不其然,你打开之后,就会自动播放音乐,返回后音乐自动关闭,就像是一个音乐的热评墙,看文字的时候带着音乐。

技巧分析

这次用了 scroll 和 page 两种效果,page的滚动的时候,没有动画过渡,也不能添加。 scroll就可以添加过渡。

代码展示

import { Override, Data, useAnimation } from "framer"

const data = Data({
    number: 0,
    y: 0,
})

export function Tap(): Override {
    return {
        onTap() {
            ;(data.number += 1), (data.y -= 14)
            controls.start({
                y: data.y,
                transition: {
                    type: "spring",
                    damping: 13,
                    stiffness: 110,
                },
            })
        },
    }
}

export function Page(): Override {
    return {
        currentPage: data.number,
    }
}
let controls
export function Scroll(): Override {
    controls = useAnimation()
    return {
        scrollAnimate: controls,
    }
}

project004

本次更新:表白墙内容页交互动效分析

演示.mp4 (10.63MB)

交互分析

页数展示

number / 31 总共是31个热评

头像和关注提示

头像-名字 —-> 头像-关注 —-> 头像-已关注 —->. 头像-名字

这个啊 其实是比较特别的。一般情况下,我们看到关注会在名字右边,点击关注后,会一直显示已关注。
云音乐这波把已关注之后,取消显示,是为了什么呢?增加了取消关注的成本,
用户需要点进已关注的那个人的个人主页才能取消。

热评的文字

image.png

我们翻页的过程中发现,每个热评的字体大小是不一样的,但是letter-spaciing 和 line-height 是一样的,所以仅仅是字号 fontSize 不一样。经过我对原UI稿的一顿分析,得出以下这么几个结论。

  • 当 字数< 44的时候,用 27pt 的字号
  • 当 字数>44 且 字数<65的时候,用 23pt 的字号
  • 当字数>65 且 字数 <152的时候,用 16pt 的字体

    !!最大的字数限制为152。这个系统筛选热评的时候,是有限制的应该。

这个其实也是我个人没怎么见过的,根据字数来控制字体的大小。
line-height 关于行高的文章,可以看一下

头像关注动效

视频展示

demo.mov (1.31MB)

demo源文件

关注.framerx.zip

技巧分析

本来想用data去做的,发现有复杂的连续运动。于是尝试着用 useAnimation 来做,然后一直报错,总有哪里不对。接下来,又看了一下官网的API。—-async—函数。这个就比较优秀了,类似于时间轴动画。建议哈,如果有时间轴动画,就可以用async函数,配合useAnimation就可以做一些复杂的时间轴动画了。

源代码

import * as React from "react"
import { Override, useAnimation } from "framer"

//autotap

export function Tap(): Override {
    async function sequence() {
        await nameControls.start({
            opacity: 0,
        })
        await bgControls.start({
            width: 88,
            transition: {
                duration: 0.25,
            },
        })
        await concernControls.start({
            opacity: 1,
            transition: {
                duration: 0.2,
            },
        })
    }
    return {
        onTap() {
            sequence()
        },
    }
}

//name

let nameControls
export function Name(): Override {
    nameControls = useAnimation()
    async function sequence() {
        await concernControls.start({
            opacity: 0,
            transition: {
                duration: 0.2,
            },
        })
        await bgControls.start({
            width: 102,
            transition: {
                duration: 0.1,
            },
        })
        await concernedControls.start({
            opacity: 1,
            transition: {
                duration: 0.2,
            },
        })
        await concernedControls.start({
            opacity: 0,
            transition: {
                duration: 0.2,
                delay: 3,
            },
        })
        await bgControls.start({
            width: 32,
            transition: {
                duration: 0.25,
            },
        })
        await nameControls.start({
            opacity: 1,
        })
    }
    return {
        animate: nameControls,
        onTap() {
            sequence()
        },
    }
}

//bg

let bgControls
export function Bg(): Override {
    bgControls = useAnimation()
    return {
        animate: bgControls,
    }
}
//concern

let concernControls

export function Concern(): Override {
    concernControls = useAnimation()
    return {
        animate: concernControls,
    }
}
//concenrned
let concernedControls

export function Concerned(): Override {
    concernedControls = useAnimation()
    return {
        animate: concernedControls,
    }
}

project005

本次更新:根据热评的文字的数量决定文字的大小

视频展示

1.mp4 (38.1MB)

demo源文件

font.framerx.zip

技巧分析

经过对UI稿件的分析,热评文字的大小大概有:17pt , 21pt , 24pt , 27pt
24 和 27 的操作搞不懂啊,同样字体数量的情况下,24 和 27 都有。我又看了云村的机制,热评墙的内容是从瀑布流的内容中弄出来的。 后台工程师,应该也是根据字体的数量判断的,其实我这种不是很严谨,英文和中文其实字符占的距离是不一样的,暂时先实现这个效果吧。

framer X,因为这次要根据字体数量作为判断,那用Override就不行了,就得用原生的code组件了,所以这次的代码和以前的看着也不一样。主要是用了Stack的原理,使得字体的高度变化的同时,下面的模块也能跟上去。

源代码

import * as React from "react"
import { Frame, addPropertyControls, ControlType, Stack } from "framer"
import { Music } from "./canvas"
// Open Preview: Command + P
// Learn more: https://framer.com/api

export function Font(props) {
    console.log(props.text.length)
    const textLength = props.text.length
    if (textLength < 44) {
        return (
            <Stack
                direction="vertical"
                gap={12}
                alignment="start"
                distribution="start"
            >
                <p
                    style={{
                        width: 329,
                        fontSize: 27,
                        color: "white",
                        textAlign: "left",
                        letterSpacing: 1,
                        lineHeight: 1.65,
                        fontWeight: 700,
                        //@ts-ignore
                        wordBreak: "break-Word",
                    }}
                >
                    {props.text}
                </p>
                <Music />
            </Stack>
        )
    } else if (textLength >= 44 && textLength < 65) {
        return (
            <Stack
                direction="vertical"
                gap={12}
                alignment="start"
                distribution="start"
            >
                <p
                    style={{
                        width: 329,
                        fontSize: 21,
                        color: "white",
                        textAlign: "left",
                        letterSpacing: 1,
                        lineHeight: 1.65,
                        fontWeight: 700,
                        //@ts-ignore
                        wordBreak: "break-Word",
                    }}
                >
                    {props.text}
                </p>
                <Music />
            </Stack>
        )
    } else {
        return (
            <Stack
                direction="vertical"
                gap={12}
                alignment="start"
                distribution="start"
            >
                <p
                    style={{
                        width: 329,
                        fontSize: 17,
                        color: "white",
                        textAlign: "left",
                        letterSpacing: 1,
                        lineHeight: 1.65,
                        fontWeight: 700,
                        //@ts-ignore
                        wordBreak: "break-Word",
                    }}
                >
                    {props.text}
                </p>
                <Music />
            </Stack>
        )
    }
}
Font.defaultProps = {
    text: "Get started!",
}

// Learn more: https://framer.com/api/property-controls/
addPropertyControls(Font, {
    text: {
        title: "Text",
        type: ControlType.String,
        defaultValue: "Hello Framer!",
    },
})

project006

本次更新:爱心关注动效

视频展示

ScreenFlow.mp4 (463.74KB)

demo源文件

heart动效.framerx.zip

技巧分析

这里说一下思考过程。

一开始我是想弄一个code组件,通过控制property controls的的内容来控制。后来发现,爱心的图形不行放大和缩小不行。因为Graphic的大小是不能通过代码控制的。

后来,我就想,也可以通过SVG code来实现啊。但是,发现同样一个问题,code组件中的SVG code,不能通过代码更改其大小。

再后来想了一下,也可能实际生产的过程中,程序员老哥可能是用2张png图片呢。到此,焕然大悟,所以用图片解决了问题。

Framer X知识点:useAnimation ,Data

通过onTap来控制事件的布尔类型,然后调用两个函数,一个是系列关注时会发生的动画,一个是取消时会发生的动画。

源代码

import {Override , Data , useAnimation} from 'framer'

//data
const data = Data({
    open:false
})

//tap

export function Tap():Override {
    //true
    async function trueSequence() {
        //border opacity
        await borderedControls.start({
            opacity:0,
            transition:{
                duration:.1
            }
        })
        //filled oapcity
        await filledControls.start({
            opacity:1,
            transition:{
                duration:.1
            }
        })
        //filled scale
        await filledControls.start({
            scale: 1.3,
            transition: {
                duration: 0.05
            }
        })
        await filledControls.start({
            scale: 1,
            transition: {
                duration: 0.05
            }
        })
        //concerned
        await concernedControls.start({
            opacity:1,
            transition:{
                duration:0.25,
                delay:0.15
            }
        })
        await concernedControls.start({
            opacity:0,
            transition:{
                duration:0.1,
                delay:1
            }
        })
        //filled
        await filledControls.start({
            opacity: 0.8,
            transition: {
                duration: 0.05
            }
        })
    }
    //false
    async function falseSequnce() {
        //filled opacity
        await filledControls.start({
            opacity: 0,
            transition: {
                duration: .1,
                delay:0.35
            }
        })
        //bordered opacity
        await borderedControls.start({
            opacity: 1,
            transition: {
                duration: .1
            }
        })
        //cancle
        //concerned
        await cancleControls.start({
            opacity: 1,
            transition: {
                duration: 0.25
            }
        })
        await cancleControls.start({
            opacity: 0,
            transition: {
                duration: 0.1,
                delay: 1
            }
        })
    }
    return{
        onTap(){
            data.open = !data.open
            if(data.open == true){
                trueSequence()
            }else{
                falseSequnce()
            }
        }
    }
}

//bordered

let borderedControls

export function bordered():Override {
    borderedControls = useAnimation()
    return{
        animate: borderedControls
    }
}
//filled

let filledControls

export function filled():Override {
    filledControls = useAnimation()
    return{
        animate: filledControls
    }
}
//concerned

let concernedControls

export function concerned():Override {
    concernedControls = useAnimation()
    return{
        animate: concernedControls
    }
}
//cancle

let cancleControls

export function cancle():Override {
    cancleControls = useAnimation()
    return{
        animate: cancleControls
    }
}