首先来看下日推界面的效果:
网易云音乐日推界面的亮点就是在上拉的时候,banner页面逐渐模糊。字体透明度下降,最后左上角显示出“每日推荐”的字体。
这个界面用户会觉得很清晰、便捷又不失美观(网易云音乐的AppUI设计是真的太顶)
但是去模仿去做,其实又会遇到一些坑。
比如说,这里明显使用了AppBarLayout
,图片放在CollapsingToolbarLayout
里,但是:下面的RecyclerView的有上圆角,仿佛就是View在Recycler下层一样,如果用AppBarLayout的话,图片是不可能覆盖到下面的Recycler的。
更别说RecycerView还有两个类似标签的小View。
我这里想到了两种做法:
- 整个界面自定义成一个ViewGroup,将图片、RecycleView封装,对于上滑RecyclerView做手势监听,如果被RecyclerView包裹的RelativeLayout没有到达顶部则不滑动RecyclerView BALABALBALBALALBAL。。。。
怎么说呢,我认为网易云音乐他们就是这么实现的,只要开一下开发者选项的 布局边界显示就能发现他们处理的很巧妙 - 还是用AppBarLayout实现,首先AppBarLayout是可以监听滑动的,所以便于我们做动态高斯模糊,其次,RecyclerView的上圆角没有,但是我们也是可以自己构造呀!
这里我选择第二种方法实现。低仿后的效果如下:
圆角、动态高斯模糊、字体透明度都实现了~
需要学习什么
只要学习两个就够了
- 关于
CoordinatorLayout
和AppBarLayout
的使用
我之前写过一篇Blog就是讲这个的。讲的不是很细,但是把用法和注意的点都讲解到了。其实只要学会了Behavior也够我们开发产品了。
小学CoordinatorLayout的交互 - 动态高斯模糊
学习了Android开发学习之路-动态高斯模糊怎么做 这个大神用了双ImageView重叠来做,(其实我一开始也想到了用重叠,但是我不知道ImageView.setImageAlpha(alpha)
这个方法= =!) - 沉侵式状态栏
这个emmmmm,没什么好说的吧,v21之后都好设置,网上blog讲的也很多,这里只要设置activity的theme:
差不多就行了↓
<style name="TranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
并且需要在xml文件中把CoordinatorLayout的 android:fitsSystemWindows=""不用写
我这边阐述一下动态高斯模糊的思路。
动态高斯模糊思路
首先高斯模糊的做法,主流的有两种:
1、Android自带的Renderscript
使用Android自带的API,首先要在 app.gradle的defaultConfig写入:
26(这里是minSdk)
renderscriptSupportModeEnabled true
然后通过下面的方法,传入原bitmap和模糊半径,来获取一个模糊后的bitmap:
private Bitmap blur(Bitmap bitmap, float radius) {
Bitmap output = Bitmap.createBitmap(bitmap); // 创建输出图片
RenderScript rs = RenderScript.create(this); // 构建一个RenderScript对象
ScriptIntrinsicBlur gaussianBlue = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); // 创建高斯模糊脚本
Allocation allIn = Allocation.createFromBitmap(rs, bitmap); // 创建用于输入的脚本类型
Allocation allOut = Allocation.createFromBitmap(rs, output); // 创建用于输出的脚本类型
gaussianBlue.setRadius(radius); // 设置模糊半径,范围0f<radius<=25f
gaussianBlue.setInput(allIn); // 设置输入脚本类型
gaussianBlue.forEach(allOut); // 执行高斯模糊算法,并将结果填入输出脚本类型中
allOut.copyTo(output); // 将输出内存编码为Bitmap,图片大小必须注意
rs.destroy(); // 关闭RenderScript对象,API>=23则使用rs.releaseAllContexts()
return output;
}
效果就不展示了,反正是真的可以用。效率高。
2、Glide自带的
想不到吧?
只要在导入:
'com.github.bumptech.glide:glide:4.9.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
api 'jp.wasabeef:glide-transformations:4.0.1' //高斯模糊api
然后在代码中引入:
.with(this)
.load(coverUrl)
//第一个参数时模糊半径,范围在 0<radius<=25f,第二个参数是放大参数,默认是1
.apply(RequestOptions.bitmapTransform(new BlurTransformation(25, 1)))
.into(ivBg);
思路
上面的Api其实并不是最重要的,不要以为用了这些方法,就能在监听滑动的时候去用这些方法就可以达成效果了。
这是因为 你每次滑动,就要进行这些计算,如果图片精度、分辨率很高,一次计算达到了上百ms,那我们一次滑动至少产生15次以上的计算,这必然会产生卡顿。
所以我们要做的动态模糊,其实是一个幌子,我们不是根据监听滑动进行模糊,而是让视觉效果让它变得模糊,就像是欺骗的感觉。
√:我们拿一份已经模糊的图放在低层,再拿一张完全不模糊的放在上层覆盖它,然后随着滑动,上层的透明度逐渐减低,下层逐渐显现出来,这样的视觉效果,正是一个图片逐渐模糊。
设置图片透明度使用 setImageView(alpha)
,呜呜呜我之前不知道这个方法,结果一直找不到解决方法。
实现
1、监听AppBarLayout滑动
因为AppBarLayou自带的api不太方便,我们需要再封装一层:
/**
* AppBarLayout的监听类
*/
public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener {
private static final String TAG = "AppBarStateChangeListen";
public enum State {
EXPANDED,
COLLAPSED,
IDLE
}
private State mCurrentState = State.IDLE;
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int i) {
onOffsetChanged(appBarLayout);
if (i == 0) {
if (mCurrentState != State.EXPANDED) {
onStateChanged(appBarLayout, State.EXPANDED);
}
mCurrentState = State.EXPANDED;
} else if (Math.abs(i) >= appBarLayout.getTotalScrollRange()) {
if (mCurrentState != State.COLLAPSED) {
onStateChanged(appBarLayout, State.COLLAPSED);
}
mCurrentState = State.COLLAPSED;
} else {
if (mCurrentState != State.IDLE) {
onStateChanged(appBarLayout, State.IDLE);
}
mCurrentState = State.IDLE;
}
}
//状态发生了改变
public abstract void onStateChanged(AppBarLayout appBarLayout, State state);
//发生了偏移
public abstract void onOffsetChanged(AppBarLayout appBarLayout);
}
2、XML文件
说来你们可能不信,我对圆角的处理是这样的:
蓝色和天蓝色都是属于AppBarLayout,上滑需要对圆角layout设置layout_collapseMode="parallax"
然后 AppBarLayout最底层仿两个ImageView,模糊的在下面,清晰的在上面。
并且 AppBarLayout的 app:elevation
要设置为 0dp,不然在折叠的时候会产生阴影,影响白色圆角的发挥
而且注意 是 app的elevation而不是 android的 elevation
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:elevation="0dp">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_200"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
//低层模糊图片
<ImageView
android:id="@+id/iv_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
//上层做透明度变化的图片
<ImageView
android:id="@+id/iv_background_cover"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
//一些字体
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_collapseMode="parallax">
<TextView
android:id="@+id/tv_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_month"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
//标题头
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/ClubToolbar"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_55"
android:layout_marginTop="@dimen/dp_30"
app:layout_collapseMode="pin">
<include
android:id="@+id/title"
layout="@layout/common_title" />
</android.support.v7.widget.Toolbar>
//哈哈哈哈哈这个就是一个圆角矩形
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_10"
android:layout_marginTop="@dimen/dp_290"
android:background="@drawable/bg_dailyrecommend"
app:layout_collapseMode="pin" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<RelativeLayout
android:id="@+id/rl_play"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:paddingBottom="@dimen/dp_40"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
3、根据监听AppBarLayout来改变透明度
AppBarLayout的滑动监听是没有 偏移值 offsetX和offsetY的,所以滑动到一定距离,字体的透明度到一定值怎么弄呢,我这里借助的是包裹着RecyclerView的RelativeLayout距离顶部的距离做判断。
该layout的 getTop() 越接近顶部,说明AppBarLayout快要折叠,我根据这个值去计算。
从上图可以知道 我们处理getTop()返回在 Toobar高度~AppBarLayout高度.
如果在Toolbar设置了marginTop值,则也要在Toolbar高度里加上,我这里就设置了(因为使用了沉侵状态栏)。
首先在onCreate中初始化这些值:
//计算 getTop移动范围,和初始化图片
protected void initData() {
if (coverUrl != null) {
Glide.with(this)
.load(coverUrl)
.into(ivBgCover);
Glide.with(this)
.load(coverUrl)
.apply(RequestOptions.bitmapTransform(new BlurTransformation(25, 1)))
.into(ivBg);
}
//距离最小值
minDistance = DensityUtil.dp2px(DailyRecommendActivity.this, 85);
//距离范围差值
deltaDistance = DensityUtil.dp2px(DailyRecommendActivity.this, 200) - minDistance;
}
然后对appBarLayout进行监听:
.addOnOffsetChangedListener(new AppBarStateChangeListener() {
@Override
public void onStateChanged(AppBarLayout appBarLayout, AppBarStateChangeListener.State state) {
if (state == State.COLLAPSED) {
//下面的方法会出现监听不到的情况,所以这里变成折叠状态,最好再设置一次透明度
setLeftTitleAlpha(255f);
}
}
@Override
public void onOffsetChanged(AppBarLayout appBarLayout) {
//获取滑动进度
float alphaPercent = (float) (rlPlay.getTop() - minDistance) / (float) deltaDistance;
int alpha = (int) (alphaPercent * 255);
LogUtil.d(TAG, "alpha : " + alpha);
ivBgCover.setImageAlpha(alpha);
tvMonth.setAlpha(alphaPercent);
tvDay.setAlpha(alphaPercent);
if (alphaPercent < 0.2f) {
float leftTitleAlpha = (1.0f - alphaPercent / 0.2f);
setLeftTitleAlpha(leftTitleAlpha);
} else {
setLeftTitleAlpha(0);
}
}
});
setLeftTitleAlpha
就是设置左上角“每日推荐”的透明度,在网上 滑动最后 20%进度的时候,让他慢慢浮现出来。
到这里基本就已经实现了仿日推界面了。
小结
这个界面加上之前做的一个 让View一般透明的那个View算是已经完成网易云的日推模块了。
这篇blog主要是让自己 对 AppBarLayout
学以致用,然后在上面加一些体用更棒的东西。
比如说 动态高斯模糊、沉侵式状态栏。
有些东西看起来不难,最后去实现的时候也不是很难,但是 去思考实现方案、去动手做的过程才是最能令人成长的地方。
所以多思考,看到喜欢的界面,去想一想,做一做。做个低配、中配版的出来~