简介
最近做一个新闻列表功能时,设计师设计了一个时间轴新闻列表,同一个日期第一条显示月日,并且滑动到顶端时悬浮,效果如下:
效果图
看了效果之后,考虑过第一条可见悬停,发现右侧还有时间且可以看到整个滑动过程,所以这条方案就pass,还是使用最简单实现方式,悬浮的时间布局在Activity布局上。
实现
Activity布局
RecyclerView与时间空间叠加布局,LinearLayout包裹可以控制布局在父布局的位置,保证RecycleView第一个item与其重合,这样视觉看不出是同一个。
<?xml version="1.0" encoding="utf-8"?>
<com.scwang.smartrefresh.layout.SmartRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!--内容-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rl_new"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!--悬浮时间控件-->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_10"
android:layout_marginLeft="@dimen/dp_10"
android:paddingTop="@dimen/dp_10">
<include layout="@layout/item_time_layout" />
</LinearLayout>
</FrameLayout>
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
悬浮时间空间布局
悬浮控件可自定义不同颜色样式
<?xml version="1.0" encoding="utf-8"?>
<!-- 时间框-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/time_layout"
android:layout_width="@dimen/dp_30"
android:layout_height="wrap_content"
android:background="@drawable/time_bg_color"
android:paddingStart="@dimen/dp_5"
android:paddingEnd="@dimen/dp_5">
<!-- 日-->
<TextView
android:id="@+id/tv_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:maxLines="2"
android:text="@string/defalut_"
android:textColor="@color/colorAccent"
android:textSize="@dimen/text_size_12"
android:textStyle="bold" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_day"
android:layout_centerHorizontal="true"
android:layout_marginTop="-2dp">
<!-- 月-->
<TextView
android:id="@+id/tv_month"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:text="@string/defalut_"
android:textColor="@color/colorAccent"
android:textSize="@dimen/text_size_10" />
<!-- 月单位-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:text="@string/month"
android:textColor="@color/colorAccent"
android:textSize="@dimen/text_size_10" />
</androidx.appcompat.widget.LinearLayoutCompat>
</RelativeLayout>
Adapter布局
其中父布局使用LinearLayout可以动态的根据子控件的内容宽度决定🔗线的长度,考虑item之间需用细线连接,父布局上下不能出现间隔样式。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_10"
android:layout_marginEnd="@dimen/dp_10"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 普通时间线-->
<LinearLayout
android:id="@+id/rl_line"
android:layout_width="@dimen/dp_30"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<!-- 上半部分竖线-->
<View
android:id="@+id/view_top"
android:layout_width="@dimen/dp_1"
android:layout_height="@dimen/dp_10"
android:layout_gravity="center_horizontal"
android:background="@color/colorAccent" />
<!-- 中间点-->
<ImageView
android:id="@+id/dot_img"
android:layout_width="@dimen/dp_10"
android:layout_height="@dimen/dp_10"
android:layout_gravity="center_horizontal"
android:contentDescription="@string/contentDescription"
android:src="@drawable/ic_hongdian" />
<!--下半部分竖线-->
<View
android:id="@+id/v_line"
android:layout_width="@dimen/dp_1"
android:layout_height="0dp"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
android:background="@color/colorAccent" />
</LinearLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="-10dp"
android:layout_marginLeft="-10dp"
android:layout_marginTop="@dimen/dp_5"
android:layout_toEndOf="@+id/rl_line"
android:layout_toRightOf="@+id/rl_line"
android:orientation="horizontal">
<!--横向线-->
<View
android:id="@+id/view_line"
android:layout_width="@dimen/dp_20"
android:layout_height="@dimen/dp_1"
android:layout_gravity="center_vertical"
android:layout_toEndOf="@+id/dot_img"
android:layout_toRightOf="@+id/dot_img"
android:background="@color/colorAccent" />
<!--详细时间-->
<TextView
android:id="@+id/tv_title_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/view_line"
android:layout_toRightOf="@+id/view_line"
android:background="@drawable/shape_time_bg"
android:gravity="center"
android:paddingLeft="@dimen/dp_8"
android:paddingTop="@dimen/dp_2"
android:paddingRight="@dimen/dp_8"
android:paddingBottom="@dimen/dp_2"
android:text="@string/defalut_"
android:textColor="@android:color/white"
android:textSize="@dimen/text_size_10" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 时间框-->
<include layout="@layout/item_time_layout" />
<!-- 内容-->
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/ll_time"
android:layout_marginStart="@dimen/dp_10"
android:layout_marginLeft="@dimen/dp_10"
android:layout_marginTop="@dimen/dp_5"
android:layout_toEndOf="@+id/rl_line"
android:layout_toRightOf="@+id/rl_line"
android:orientation="vertical"
android:paddingBottom="@dimen/dp_8">
<!-- 新闻标题-->
<TextView
android:textColor="@android:color/black"
android:id="@+id/tv_news_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lineSpacingExtra="@dimen/dp_5"
android:maxLines="2"
android:textSize="@dimen/text_size_14" />
<!-- 底部详细信息-->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_5"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_5"
android:layout_marginRight="@dimen/dp_5"
android:textSize="@dimen/text_size_12" />
<TextView
android:id="@+id/tv_source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_5"
android:layout_marginRight="@dimen/dp_5"
android:textSize="@dimen/text_size_12" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_5"
android:layout_marginLeft="@dimen/dp_5"
android:textSize="@dimen/text_size_12" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 新闻图片-->
<ImageView
android:id="@+id/new_pic"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_80"
android:layout_marginTop="@dimen/dp_8"
android:contentDescription="@string/contentDescription"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher" />
</androidx.appcompat.widget.LinearLayoutCompat>
</RelativeLayout>
</LinearLayout>
Adapter数据绑定
这里我们为了简化adapter的使用,依赖
com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.0-beta11
在里面实现时间框的显示、隐藏,时间标签的颜色改变,条目内容的数据绑定,内容详情显示隐藏,图片显示
package com.zj.timeaxis
/**
* 新闻适配器
*
* @author zj
*/
class NewsListAdapter(layoutResId: Int) :
BaseQuickAdapter<NewsBean?, BaseViewHolder>(layoutResId) {
@SuppressLint("SimpleDateFormat")
var sdfDay = SimpleDateFormat("yyyy-MM-dd")
@SuppressLint("SimpleDateFormat")
var sdfHour = SimpleDateFormat("HH:mm:ss")
override fun convert(helper: BaseViewHolder, item: NewsBean?) {
item?.let {
val position = helper.adapterPosition
//前个条目时间
val datePre: Long = data[Math.max(position - 1, 0)]!!.time
//当前条目
val date: Long = item.time
val ca: Calendar = Calendar.getInstance()
ca.timeInMillis = date
//当前条目时间
val caPre: Calendar = Calendar.getInstance()
caPre.timeInMillis = datePre
//item绑定数据
helper.setText(R.id.tv_news_title, item.title)
.setText(R.id.tv_title_time, sdfHour.format(ca.time))
.setText(R.id.tv_source, item.source)
val viewTop: View = helper.getView(R.id.view_top)
val monthRl: RelativeLayout = helper.getView(R.id.time_layout)
val tvDay: TextView = helper.getView(R.id.tv_day)
val tvMonth: TextView = helper.getView(R.id.tv_month)
val tvTAG: TextView = helper.getView(R.id.tv_tag)
val titleTime: TextView = helper.getView(R.id.tv_title_time)
if (0 == position) {
//设置首个条目上间距
helper.itemView.setPadding(0, ScreenUtil.dip2px(context, 10f), 0, 0)
val color = ContextCompat.getColor(context, android.R.color.holo_red_light)
tvTAG.setTextColor(color)
tvTAG.text = "推荐"
tvTAG.visibility = View.VISIBLE
titleTime.setBackgroundResource(R.drawable.shape_time_bg)
} else if (1 == position) {
helper.itemView.setPadding(0, 0, 0, 0)
val color = ContextCompat.getColor(context, android.R.color.holo_blue_bright)
tvTAG.setTextColor(color)
tvTAG.text = "热门"
tvTAG.visibility = View.VISIBLE
titleTime.setBackgroundResource(R.drawable.shape_time_bg_grey)
} else { //防止复用设置间距
helper.itemView.setPadding(0, 0, 0, 0)
tvTAG.visibility = View.GONE
titleTime.setBackgroundResource(R.drawable.shape_time_bg_grey)
}
//时间框绑定
val dayInt = ca[Calendar.DATE]
val day = if (dayInt < 10) "0$dayInt" else dayInt.toString()
val monthInt = ca[Calendar.MONTH] + 1
val month = monthInt.toString()
if (0 == position) {
//红点
helper.setImageResource(R.id.dot_img, R.drawable.ic_hongdian)
//上画线
viewTop.visibility = View.INVISIBLE
monthRl.visibility = View.VISIBLE
tvDay.text = day
tvMonth.text = month
} else {
viewTop.visibility = View.VISIBLE
//判断前后时间是否一致
val condition = TextUtils.equals(sdfDay.format(ca.time), sdfDay.format(caPre.time))
if (condition) {
helper.setImageResource(R.id.dot_img, R.drawable.ic_huidian)
//这里为了区分是不是没有还是临时隐藏
monthRl.visibility = View.GONE
} else {
helper.setImageResource(R.id.dot_img, R.drawable.ic_hongdian)
titleTime.setBackgroundResource(R.drawable.shape_time_bg)
monthRl.visibility = View.VISIBLE
tvDay.text = day
tvMonth.text = month
}
}
}
}
}
悬浮时间框的逻辑控制
package com.zj.timeaxis
class MainActivity : AppCompatActivity() {
lateinit var adpter: NewsListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 这里为了区别adapter布局时间控件的id使用findViewById
val timeLayout = findViewById<RelativeLayout>(R.id.time_layout)
val tvDay = findViewById<TextView>(R.id.tv_day)
val tvMonth = findViewById<TextView>(R.id.tv_month)
//设置下拉刷新
refreshLayout.setOnRefreshListener(object : OnRefreshListener {
override fun onRefresh(refreshlayout: RefreshLayout) {
val initData = initData()
adpter.setNewData(initData)
refreshlayout.finishRefresh(2000 /*,false*/) //传入false表示刷新失败
}
})
//上拉加载更多
refreshLayout.setOnLoadMoreListener(object : OnLoadMoreListener {
override fun onLoadMore(refreshlayout: RefreshLayout) {
val initData = initData()
initData?.let { adpter.addData(it) }
refreshlayout.finishLoadMore(2000 /*,false*/) //传入false表示加载失败
}
})
//初始化RecycleView
val linearLayoutManager = LinearLayoutManager(this)
rl_new.layoutManager = linearLayoutManager
adpter = NewsListAdapter(R.layout.item_layout_new)
rl_new.adapter = adpter
// 当RecycleView初始化完成复制悬浮框
rl_new.viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (linearLayoutManager.itemCount > 0) {
timeLayout.visibility = View.VISIBLE
val data = adpter.data
//获取第一个可见条目的position
val firstVisPos = linearLayoutManager.findFirstVisibleItemPosition()
//获取第一个可见条目的position的数据
val itemData = data.get(firstVisPos)
//格式化时间
val ca: Calendar = Calendar.getInstance()
ca.timeInMillis = itemData!!.time
val dayInt = ca[Calendar.DATE]
val day = if (dayInt < 10) "0$dayInt" else dayInt.toString()
val monthInt = ca[Calendar.MONTH] + 1
val month = monthInt.toString()
//时间控件赋值
tvDay.text = day
tvMonth.text = month
} else {
// 没有数据隐藏时间悬浮
timeLayout.visibility = View.GONE
}
}
})
rl_new.addOnScrollListener(object : RecyclerView.OnScrollListener() {
var linearLayoutManager = rl_new.getLayoutManager() as LinearLayoutManager
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
//获取第一个可见条目的position
val firstVisPos = linearLayoutManager.findFirstVisibleItemPosition()
//下滑情况下 下滑为负值 上滑为正值
val firstVisiableChildView: View? =
linearLayoutManager.findViewByPosition(firstVisPos) //gridLayoutManager布局管理器
val timeLayout: RelativeLayout? =
adpter.getViewByPosition(firstVisPos, R.id.time_layout) as RelativeLayout
//获取item条目的高度
val itemHeight = firstVisiableChildView?.getHeight();
//第一个条目标签滑动瞬间隐藏
if (firstVisiableChildView!!.getTop() < 0) {
//如果显示的状态隐藏 GONE的item不变
if (timeLayout?.visibility == View.VISIBLE) {
//设置成INVISIBLE 为了区分没有标签和暂时隐藏
timeLayout.visibility = View.INVISIBLE
}
}
//当条目数量大于2个考虑下个显示情况
if (linearLayoutManager.itemCount > 1) {
//获取第一个可见item的下一个
val nextVisPos = firstVisPos + 1
val timeLayoutNext: RelativeLayout? =
adpter.getViewByPosition(nextVisPos, R.id.time_layout) as RelativeLayout
//除第一个之外的包含标签的隐藏 Activity 浮动的标签距离顶部10dp
if (itemHeight!! + firstVisiableChildView.getTop() <= ScreenUtil.dip2px(
this@MainActivity,
10f
)
) {
//滑倒标签位置隐藏 GONE的item不变
if (timeLayoutNext?.visibility == View.VISIBLE) {
timeLayoutNext.visibility = View.INVISIBLE
}
} else {
//滑出标签位置显示 GONE的item不变
if (timeLayoutNext?.visibility == View.INVISIBLE) {
timeLayoutNext.visibility = View.VISIBLE
}
}
}
}
})
val initData = initData()
adpter.setNewData(initData)
}
/**
* 模拟不同天的数据
*/
fun initData(): MutableList<NewsBean?>? {
val newList: MutableList<NewsBean?> = ArrayList();
for (index in 1..100) {
val new: NewsBean;
when (index / 10) {
0 -> {
new = NewsBean(System.currentTimeMillis(), "新闻标题" + index, "CCTV")
}
1 -> {
new = NewsBean(System.currentTimeMillis() - 100000000, "新闻标题" + index, "湖南卫视")
}
2 -> {
new = NewsBean(System.currentTimeMillis() - 100000000 * 2, "新闻标题" + index, "江苏卫视")
}
3 -> {
new = NewsBean(System.currentTimeMillis() - 100000000 * 3, "新闻标题" + index, "浙江卫视")
}
4 -> {
new = NewsBean(System.currentTimeMillis() - 100000000 * 4, "新闻标题" + index, "江苏卫视")
}
5 -> {
new = NewsBean(System.currentTimeMillis() - 100000000 * 5, "新闻标题" + index, "江苏卫视")
}
6 -> {
new = NewsBean(System.currentTimeMillis() - 100000000 * 6, "新闻标题" + index, "江苏卫视")
}
7 -> {
new = NewsBean(System.currentTimeMillis() - 100000000 * 7, "新闻标题" + index, "江苏卫视")
}
8 -> {
new = NewsBean(System.currentTimeMillis() - 100000000 * 8, "新闻标题" + index, "江苏卫视")
}
9 -> {
new = NewsBean(System.currentTimeMillis() - 100000000 * 9, "新闻标题" + index, "江苏卫视")
}
else -> new =
NewsBean(System.currentTimeMillis() - 100000000 * 10, "新闻标题" + index, "CCTV")
}
newList.add(new)
}
return newList
}
}
结束
喜欢您就STAR一下~
github 地址