仿写Social Steps的ToolBar效果
前段时间在medium上看到一篇比较有意思的文章Toolbar Delight。该篇文章讲解了如何实现下面这种效果:
gif效果不好,想看清晰的版本请看原始文章的视频。
文章虽好,但是代码不全,有些细节作者其实也没有透露。于是我大致看了之后决定自己实现一个类似的效果,相似程度95以上吧。
其实这种还是很简单的,都是些细节问题,大致可以分解为:
-
从左到右边的渐变,这个很简单。
-
滚动的时候弧度随着 AppBarLayout 的 verticalOffset 发生变化,当折叠的时候,颜色逐渐过渡到colorPrimary,同时云彩也在折叠的时候往边界跑。
-
不同时间颜色是不一样的,太阳或者月亮的位置也尽量模拟真实世界。这个不难,把一天的时间分段处理就好了。
-
当打开界面的时候,有一个从上一个时间段状态过渡到当前状态的动画。我这里的实现效果跟原文略有区别,但是要做到跟文章完全吻合也很简单。
至于太阳,星星,云彩,都是bitmap,反编译Social Steps得到的。
好吧,编不下去了,直接看我最终实现的效果:
以上是晚上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>
图片资源就自己反编译吧。