前段时间在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>
图片资源就自己反编译吧。