前言
我又來了!前段時間由于夏天的燥熱與自身的懶惰,一直沒有更新博客哈哈哈。今天良心發(fā)現,來更一個比較有意思的東西,如題——仿全歷史APP的全沉浸時間軸實現。
話不多說,先上圖

這張圖呢,就是全歷史App中全古古跡功能的界面圖。
顯然,它通過沉浸狀態(tài)欄、透明背景、recyclerView的自定義Item等,實現了一個很優(yōu)秀的界面效果。
今天,我要做的就是猜測這效果背后的實現原理,并仿制一個類似的界面
正文
解構
我們先看看結構一下這效果背后的組成部分:

總的來講,實現這樣的效果主要需要四個部分:
1.沉浸狀態(tài)欄、透明toolbar與背景
2.TabLayout樣式自定義
3.TextSwitcher文字切換(個人猜測是有可能是使用TextSwitcher實現的,實際今天的仿制里不涉及這個哈,今天的重點是沉浸與時間軸。如果這個也包含,篇幅就控制不住了......)
4.ItemDecoration定制,繪圖畫出時間軸效果
實現
因為涉及到的內容比較多,所以只會放出部分關鍵的代碼哈。
1.沉浸狀態(tài)欄與透明背景
狀態(tài)欄的沉浸涉及到不同版本、不同情景狀態(tài)下的適配,相對來講比較復雜。想要研究的比較深的朋友,可以查看http://www.itdecent.cn/p/dc20e98b9a90這篇博客,內容充實,講的也非常詳細,本文的的沉浸適配也對其進行了參考。
我們今天涉及到的沉浸狀態(tài)欄實際非常簡單,準確的講應該叫透明狀態(tài)欄,也并不包含交互效果下狀態(tài)欄的變換。
本文采用了透明主題的配置,如下所示:
API21 之后(也就是 android 5.0 之后)的狀態(tài)欄,會默認覆蓋一層半透明遮罩。且為了保持4.4以前系統正常使用,故需要三份 style 文件,即默認的values(不設置狀態(tài)欄透明)、values-v19、values-v21(解決半透明遮罩問題)>
//valuse
<style name="TranslucentTheme" parent="AppTheme">
</style>
// values-v19。v19 開始有 android:windowTranslucentStatus 這個屬性
<style name="TranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
// values-v21。5.0 以上提供了 setStatusBarColor() 方法設置狀態(tài)欄顏色。
<style name="TranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">true</item>
<!--Android 5.x開始需要把顏色設置透明,否則導航欄會呈現系統默認的淺灰色-->
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
設置了透明主題之后,實際會布局會頂到狀態(tài)欄,因而需要為toolbar設置一個paddingTop
/**
* 設置頂部statusBar與頂部View同背景
*
* @param activity 需要設置的activity
* @param topView 頂部需要設置padding的view
*/
public static void setStatusBarMergeWithTopView(Activity activity, final View topView) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
final int statusBarHeight = getStatusBarHeight(activity);
topView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
topView.setPadding(0, statusBarHeight, 0, 0);
topView.getLayoutParams().height = topView.getHeight() + statusBarHeight;
topView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
}
對應Acitivity設置:
StatusBarUtil.setStatusBarMergeWithTopView(this, tbHistory);//tbHistory為需要設置的透明toolbar,會在下面的布局中看到。
Acitivty布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@drawable/historytest"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tb_history"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:layout_collapseMode="pin"
android:background="@android:color/transparent"
android:minHeight="?actionBarSize"
app:navigationIcon="@drawable/ic_personal_center_back"
app:title="">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="全古跡"
android:textColor="@android:color/white"
android:textSize="@dimen/text_size_xlarge_18"
android:visibility="visible"
android:textStyle="bold" />
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tl_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable"
app:tabGravity="fill"
app:tabTextAppearance="@style/HistoryTestTabLayoutTextStyle"
app:tabIndicatorHeight="0dp"
app:tabPadding="@dimen/height_2"
app:tabSelectedTextColor="@color/white"
app:tabTextColor="@color/normal_grey"/>
<androidx.viewpager.widget.ViewPager
android:id="@+id/vp_fg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_2"
android:layout_marginBottom="@dimen/margin_min_2" />
</LinearLayout>
這個布局文件除了Tablayout外并沒有什么好講的,所以我們直接進入第二部分
2.Tablayout自定義
全古跡中的tablayout主要就是進行了tab切換后字體的大小、粗細的變化,tab采用了滾動的模式,下劃線Indicator是自定義的一個較短的下劃線。
tablayout的下劃線默認是與Tab等長的,后來官方提供了app:tabIndicatorFullWidth="true"這個屬性來實現下劃線與字體等長。
但是顯然,達不到圖中的那種效果。
想要實現圖中的效果,主要有兩種方法,一種是通過反射拿到該控件來設置長度,一種是自定義一個tabView。
這里我們選擇了第二種方式。
1.注意在tablayout布局文件中設置app:tabIndicatorHeight="0dp"。
2.編寫自定義TabView布局文件。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal">
<TextView
android:textSize="14sp"
android:id="@+id/tv_tab_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:textColor="@color/white" />
<View
android:id="@+id/v_indicator"
android:layout_width="14dp"
android:layout_height="2dp"
android:layout_below="@+id/tv_tab_title"
android:layout_centerHorizontal="true"
android:layout_marginTop="5dp"
android:visibility="invisible"
android:background="#ffd700"/>
</RelativeLayout>
在對應的FragmentPagerAdapter中加上
public View getTabView(int position) {
View tabView = LayoutInflater.from(getApplicationContext()).inflate(R.layout.item_tab_layout_history, null);
TextView tabTitle = (TextView) tabView.findViewById(R.id.tv_tab_title);
tabTitle.setText(getPageTitle(position));//需要對應設置的tab標題
return tabView;
}
接下來就是設置viewPagerAdapter,以及監(jiān)聽tab選中事件。
ViewPagerAdapter viewPagerAdapter =new ViewPagerAdapter(getSupportFragmentManager());
vpFg.setAdapter(viewPagerAdapter);
vpFg.setOffscreenPageLimit(0);
tlHistory.setupWithViewPager(vpFg);
vpFg.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tlHistory));
for (int i = 0; i < tlHistory.getTabCount(); i++) {//設置自定義tabView
View tabView = viewPagerAdapter.getTabView(i);
if(i==0){//默認先加載第一個,所以第一個的字體等樣式先設置一下。
View indicator = tabView.findViewById(R.id.v_indicator);
indicator.setVisibility(View.VISIBLE);
TextView tabTitle = (TextView) tabView.findViewById(R.id.tv_tab_title);
tabTitle.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
tabTitle.setTextSize(18);
tabTitle.setTextColor(getResources().getColor(R.color.beige_yellow));
}
tlHistory.getTabAt(i).setCustomView(tabView);
}
tlHistory.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
//選中,則更改樣式
View tabView = tab.getCustomView();
if(tabView!=null){
TextView tabTitle = (TextView) tabView.findViewById(R.id.tv_tab_title);
tabTitle.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
tabTitle.setTextSize(18);
View indicator = tabView.findViewById(R.id.v_indicator);
indicator.setVisibility(View.VISIBLE);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
//未選中,則恢復樣式
View tabView = tab.getCustomView();
if(tabView!=null){
TextView tabTitle = (TextView) tabView.findViewById(R.id.tv_tab_title);
tabTitle.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL));
tabTitle.setTextSize(14);
View indicator = tabView.findViewById(R.id.v_indicator);
indicator.setVisibility(View.INVISIBLE);
}
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
vpFg.setCurrentItem(0);
vpFg.setOffscreenPageLimit(3);
tlHistory.getTabAt(0).select();
直到這里,已經完成了大半部分嘍,還剩最后的時間軸的繪制。
3.時間軸繪制
時間軸的繪制呢,我們需要借用設置itemDecoration。
什么是itemDecoration?我們來看下官方的定義:
An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.>
ItemDecoration允許應用結合adapter的數據集,對特定的item添加繪制一個周邊圖案。可以用于給items之間添加分割線、高亮裝飾效果或者分組邊界等等。>
顧名思義,ItemDecoration就是對item起裝飾作用,其最常見的用法就是添加分割線。
在這里的時間軸,需要畫出圓環(huán)、圓點、線段、文字。
完整代碼如下:
package com.example.ctccp.personalcenter.ui.adapter;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.View;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class TimeAxisItemDecoration extends RecyclerView.ItemDecoration {
private Paint LinePaint;
private Paint TextPaint;
private Paint PointPaint;
private List<String> dynasty;
private int divide_width = 66;
public TimeAxisItemDecoration(Context context, List<String> dynasty) {
LinePaint = new Paint();
TextPaint = new Paint();
PointPaint =new Paint();
PointPaint.setStyle(Paint.Style.FILL);
PointPaint.setStrokeWidth(8);
PointPaint.setStrokeCap(Paint.Cap.ROUND);
PointPaint.setColor(Color.WHITE);
LinePaint.setColor(Color.WHITE);
TextPaint.setColor(Color.WHITE);
this.dynasty = dynasty;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
int childCount = parent.getChildCount();
RecyclerView.LayoutManager manager = parent.getLayoutManager();//為了動態(tài)獲取
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
int cx = manager.getLeftDecorationWidth(child) / 2;
int cy = child.getTop()-8;
float circleRadius = 14;
int TextSize = 32;
LinePaint.setStrokeWidth((float) 2.0);
LinePaint.setStyle(Paint.Style.STROKE);
c.drawCircle(cx,cy,circleRadius,LinePaint);//圓環(huán)
c.drawPoint(cx,cy, PointPaint);//圓點
c.drawLine(cx, child.getTop(), cx, child.getBottom()+child.getHeight(), LinePaint);//線
TextPaint.setTextSize(TextSize);
}
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
RecyclerView.LayoutManager manager = parent.getLayoutManager();//為了動態(tài)獲取
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
int cx = manager.getLeftDecorationWidth(child) ;
int index = parent.getChildAdapterPosition(child);
if(index<dynasty.size())
c.drawText(dynasty.get(index), cx+8 , child.getTop() , TextPaint);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if(parent.getLayoutManager()instanceof LinearLayoutManager){
outRect.set(divide_width,divide_width,divide_width/2,0);
}
//outRect outRect就是表示在item的上下左右所撐開的距離
//View view:是指當前Item的View對象
//RecyclerView parent: 是指RecyclerView 本身
//RecyclerView.State state:通過State可以獲取當前RecyclerView的狀態(tài),也可以通過State在RecyclerView各組件間傳遞參數
}
}
成果
因為重點是沉浸和時間軸所以沒有實現TextSwitcher的部分,數據也是統一填充的(害 ,懶就一個字),實際toolbar使用的是默認尺寸,toolbar高度再低點,應該視覺效果會更好一點。

后記
全歷史實屬一個良心、炫酷的app(害,全歷史應該給我打5毛到卡里,我這還免費宣傳了一波)。
溜了溜了。