仿写Social Steps的ToolBar效果

前段时间在medium上看到一篇比较有意思的文章Toolbar Delight。该篇文章讲解了如何实现下面这种效果:

Untitled.gif

gif效果不好,想看清晰的版本请看原始文章的视频。

文章虽好,但是代码不全,有些细节作者其实也没有透露。于是我大致看了之后决定自己实现一个类似的效果,相似程度95以上吧。

其实这种还是很简单的,都是些细节问题,大致可以分解为:

  1. 从左到右边的渐变,这个很简单。

  2. 滚动的时候弧度随着 AppBarLayout 的 verticalOffset 发生变化,当折叠的时候,颜色逐渐过渡到colorPrimary,同时云彩也在折叠的时候往边界跑。

  3. 不同时间颜色是不一样的,太阳或者月亮的位置也尽量模拟真实世界。这个不难,把一天的时间分段处理就好了。

  4. 当打开界面的时候,有一个从上一个时间段状态过渡到当前状态的动画。我这里的实现效果跟原文略有区别,但是要做到跟文章完全吻合也很简单。

至于太阳,星星,云彩,都是bitmap,反编译Social Steps得到的。

好吧,编不下去了,直接看我最终实现的效果:

Untitled.gif

以上是晚上19.44的效果,其它时间段就不一一上图了。

大部分效果都是在一个叫做ToolbarArcBackground的自定义view中实现的:

ToolbarArcBackground.java

public class ToolbarArcBackground extends View {
    private float scale = 1;
    private float timeRate;
    private int gradientColor1 = 0xff4CAF50;
    private int gradientColor2 = 0xFF0E3D10;
    private int lastGradientColor1 = 0xff4CAF50;
    private int lastGradientColor2 = 0xFF0E3D10;
    private Context context;
    private Bitmap sun;
    private Bitmap sunMorning;
    private Bitmap sunNoon;
    private Bitmap sunEvening;
    private Bitmap cloud1;
    private Bitmap cloud2;
    private Bitmap cloud3;
    private Bitmap moon;
    private Bitmap star;
    private int cloud1X = 50;
    private int cloud2X = 450;
    private int cloud3X = 850;
    int waveHeight = 60;
    private int cloud1Y = waveHeight + 150;
    private int cloud2Y = waveHeight + 120;
    private int cloud3Y = waveHeight + 20;
    private int sunHeight;
    private Day now = Day.MORNING;
    enum Day {
        MORNING, NOON, AFTERNOON, EVENING,
        MIDNIGHT
    }
    public ToolbarArcBackground(Context context) {
        super(context);
        this.context = context;
        init();
    }
    public ToolbarArcBackground(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        init();
    }
    public ToolbarArcBackground(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        init();
    }
    public void setScale(float scale) {
        this.scale = scale;
        invalidate();
    }
    private void init() {
        calculateTimeLine();
        createBitmaps();
        initGradient();
    }
    private void initGradient(){
        switch (now) {
            case MORNING:
                lastGradientColor1 = ContextCompat.getColor(context, R.color.toolbar_gradient_1_midnight);
                lastGradientColor2 = ContextCompat.getColor(context, R.color.toolbar_gradient_2_midnight);
                gradientColor1 = ContextCompat.getColor(context, R.color.toolbar_gradient_1_morning);
                gradientColor2 = ContextCompat.getColor(context, R.color.toolbar_gradient_2_morning);
                break;
            case NOON:
                lastGradientColor1 = ContextCompat.getColor(context, R.color.toolbar_gradient_1_morning);
                lastGradientColor2 = ContextCompat.getColor(context, R.color.toolbar_gradient_2_morning);
                gradientColor1 = ContextCompat.getColor(context, R.color.toolbar_gradient_1_noon);
                gradientColor2 = ContextCompat.getColor(context, R.color.toolbar_gradient_2_noon);
                break;
            case AFTERNOON:
                lastGradientColor1 = ContextCompat.getColor(context, R.color.toolbar_gradient_1_noon);
                lastGradientColor2 = ContextCompat.getColor(context, R.color.toolbar_gradient_2_noon);
                gradientColor1 = ContextCompat.getColor(context, R.color.toolbar_gradient_1_noon_evening);
                gradientColor2 = ContextCompat.getColor(context, R.color.toolbar_gradient_2_noon_evening);
                break;
            case EVENING:
                lastGradientColor1 = ContextCompat.getColor(context, R.color.toolbar_gradient_1_noon_evening);
                lastGradientColor2 = ContextCompat.getColor(context, R.color.toolbar_gradient_2_noon_evening);
                gradientColor1 = ContextCompat.getColor(context, R.color.toolbar_gradient_1_evening);
                gradientColor2 = ContextCompat.getColor(context, R.color.toolbar_gradient_2_evening);
                break;
            case MIDNIGHT:
                lastGradientColor1 = ContextCompat.getColor(context, R.color.toolbar_gradient_1_evening);
                lastGradientColor2 = ContextCompat.getColor(context, R.color.toolbar_gradient_2_evening);
                gradientColor1 = ContextCompat.getColor(context, R.color.toolbar_gradient_1_midnight);
                gradientColor2 = ContextCompat.getColor(context, R.color.toolbar_gradient_2_midnight);
                break;
        }
    }
    private void calculateTimeLine() {
        Date d = new Date();
        if (d.getHours() > 5 && d.getHours() < 11) {
            now = Day.MORNING;
        } else if (d.getHours() < 13 && d.getHours() >= 11) {
            now = Day.NOON;
        } else if (d.getHours() < 18 && d.getHours() >= 13) {
            now = Day.AFTERNOON;
        } else if (d.getHours() < 22 && d.getHours() >= 18) {
            now = Day.EVENING;
        } else if (d.getHours() <= 5 || d.getHours() >= 22 && d.getHours() < 24) {
            now = Day.MIDNIGHT;
        }
        if (d.getHours() > 5 && d.getHours() < 18) {
            timeRate = ((float)d.getHours() - 5 )/ 13;//本来是12个小时,但是为了让太阳露一点出来,+1
        } else {
            if (d.getHours() < 24 && d.getHours() >= 18) {
                timeRate = (float)(d.getHours() - 17) / 12;
            } else {
                timeRate = (float)(d.getHours() + 6) / 12;
            }
        }
    }
    private void createBitmaps() {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        cloud1 = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.bg_cloud_01);
        cloud2 = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.bg_cloud_02);
        cloud3 = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.bg_cloud_03);
        sun = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.bg_sun);
        sunMorning = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.bg_sun_morning);
        sunNoon = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.bg_sun_noon);
        sunEvening = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.bg_sun_evening);
        moon = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.bg_moon);
        star = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.bg_stars);
    }
    public void startAnimate(){
        Log.i("ToolbarArcBackground", "timeRate = " + timeRate);
        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
        anim.setDuration(3000);
        //anim.setInterpolator();
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            float temp = timeRate;
            int currentGradientColor1 =gradientColor1;
            int currentGradientColor2 =gradientColor2;
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //时间段的过渡
                float currentValue = (float) animation.getAnimatedValue();
                timeRate = currentValue * temp;
                //由上一个时间段的颜色过渡到下一个时间段的颜色
                ArgbEvaluator argbEvaluator = new ArgbEvaluator();//渐变色计算类
                gradientColor1 = (int) (argbEvaluator.evaluate(currentValue, lastGradientColor1, currentGradientColor1));
                gradientColor2 = (int) (argbEvaluator.evaluate(currentValue, lastGradientColor2, currentGradientColor2));
                invalidate();
            }
        });
        anim.start();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawGradient(canvas);
        drawCloud(canvas);
        if (now == Day.MIDNIGHT || now == Day.EVENING) {
            drawStar(canvas);
            drawMoon(canvas);
        } else {
            drawSun(canvas);
        }
        drawOval(canvas);
    }
    private void drawOval(Canvas canvas) {
        Paint ovalPaint = new Paint();
        final Path path = new Path();
        ovalPaint.setColor(Color.WHITE);
        ovalPaint.setAntiAlias(true);
        path.moveTo(0, getMeasuredHeight());
        path.quadTo(getMeasuredWidth() / 2, getMeasuredHeight() - waveHeight * scale, getMeasuredWidth(), getMeasuredHeight());
        path.lineTo(0, getMeasuredHeight());
        path.close();
        canvas.drawPath(path, ovalPaint);
    }
    private void drawCloud(Canvas canvas) {
        canvas.drawBitmap(cloud1, cloud1X * scale, cloud1Y * scale, null);
        canvas.drawBitmap(cloud2, cloud2X * scale, cloud2Y * scale, null);
        canvas.drawBitmap(cloud3, cloud3X + (1 - scale) * getMeasuredWidth(), cloud3Y * scale, null);
    }
    private void drawStar(Canvas canvas) {
        canvas.drawBitmap(star, 0, 0, null);
    }
    private void drawMoon(Canvas canvas) {
        int passed =  (int)(getMeasuredWidth() * timeRate);
        int xpos = passed - moon.getWidth() / 2;
        canvas.drawBitmap(moon, xpos + (1 - scale) * getMeasuredWidth(), -50, null);
    }
    private void drawGradient(Canvas canvas) {
        Paint paint = new Paint();
        ArgbEvaluator argbEvaluator = new ArgbEvaluator();//渐变色计算类
        int changedColor1 = (int) (argbEvaluator.evaluate(1 - scale, gradientColor1, ContextCompat.getColor(context, R.color.colorPrimary)));
        int changedColor2 = (int) (argbEvaluator.evaluate(1 - scale, gradientColor2, ContextCompat.getColor(context, R.color.colorPrimary)));
        LinearGradient linearGradient = new LinearGradient(0f, 0f, getMeasuredWidth(), getMeasuredHeight(), changedColor1, changedColor2, Shader.TileMode.CLAMP);
        paint.setShader(linearGradient);
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);
//
//        LinearGradient linearGradient1 = new LinearGradient(0f, 0f, getMeasuredWidth(), getMeasuredHeight(), 0xff00d9ff, 0xff00b0ff, Shader.TileMode.CLAMP);
//        paint.setShader(linearGradient1);
//        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);
    }
    private void drawSun(Canvas canvas) {
        Log.e("rate", "timeRate = " + timeRate);
        Log.e("rate", "sun.getWidth() = " + sun.getWidth());
        int passed =  (int)(getMeasuredWidth() * timeRate);
        int xpos = passed - sun.getWidth() / 2;
        if (now == Day.MORNING) {
            canvas.drawBitmap(sunMorning, xpos + (1 - scale) * getMeasuredWidth(), -50, null);
        } else if (now == Day.NOON) {
            canvas.drawBitmap(sunNoon, xpos + (1 - scale) * getMeasuredWidth(), -50, null);
        } else if (now == Day.AFTERNOON) {
            canvas.drawBitmap(sunEvening, xpos + (1 - scale) * getMeasuredWidth(), -50, null);
        }
    }
}

布局activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        >
        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <com.jcodecraeer.day.ToolbarArcBackground
                android:id="@+id/toolbarArcBackground"
                android:layout_width="match_parent"
                android:layout_height="130dp" />
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:contentInsetEnd="0dp"
                android:contentInsetLeft="0dp"
                android:contentInsetRight="0dp"
                android:contentInsetStart="0dp"
                app:contentInsetEnd="0dp"
                app:contentInsetLeft="0dp"
                app:contentInsetRight="0dp"
                app:contentInsetStart="0dp"
                app:layout_collapseMode="pin"
                >
            </android.support.v7.widget.Toolbar>
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>
    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
    >
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/large_text" />
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

 在MainActivity中这样使用:

package com.jcodecraeer.day;
import android.support.design.widget.AppBarLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
public class MainActivity extends AppCompatActivity {
    ToolbarArcBackground mToolbarArcBackground;
    AppBarLayout mAppBarLayout;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        final ActionBar ab = getSupportActionBar();
        setTitle("");
        mAppBarLayout = (AppBarLayout) findViewById(R.id.appbar);
        mToolbarArcBackground = (ToolbarArcBackground) findViewById(R.id.toolbarArcBackground);
        mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            int scrollRange = -1;
            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                if (scrollRange == -1) {
                    scrollRange = appBarLayout.getTotalScrollRange();
                }
                float scale = (float) Math.abs(verticalOffset) / scrollRange;
                mToolbarArcBackground.setScale(1 - scale);
            }
        });
        getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run() {
                mToolbarArcBackground.startAnimate();
            }
        });
    }
}

颜色资源:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#4CAF50</color>
    <color name="colorPrimaryDark">#4CAF50</color>
    <color name="colorAccent">#FF4081</color>
    <color name="toolbar_gradient_1">#ff00d9ff</color>
    <color name="toolbar_gradient_1_evening">#341c61</color>
    <color name="toolbar_gradient_1_midnight">#ff416eb2</color>
    <color name="toolbar_gradient_1_morning">#fff0ecb3</color>
    <color name="toolbar_gradient_1_noon">#ff00d9ff</color>
    <color name="toolbar_gradient_1_noon_evening">#ffa976ed</color>
    <color name="toolbar_gradient_2">#ff00b0ff</color>
    <color name="toolbar_gradient_2_evening">#1e1918</color>
    <color name="toolbar_gradient_2_midnight">#ff2a2569</color>
    <color name="toolbar_gradient_2_morning">#ff00b3ff</color>
    <color name="toolbar_gradient_2_noon">#ff00b0ff</color>
    <color name="toolbar_gradient_2_noon_evening">#704343</color>
</resources>

图片资源就自己反编译吧。