最近项目中遇到个局部嵌套滑动的需求,实现后整理成demo
代码不多,就直接贴出来了。
需求:
1、一个界面,顶部有一块区域是 固定区域,不可以滑动;
2、中间有个内容介绍区域(控件);
3、下面是 viewpager+Fragment,多模块界面;
4、下面的 viewpager需要可以往上滑动,把内容介绍区域 顶走,固定区域不动。当 viewpager的tab到了 固定区域 的底部的时候,只能viewpager中Fragment里面的布局可以滑动
示例界面:
需要被顶走的区域
往上滑动时,把 内容介绍 布局顶走后的界面
数据列表可以滑动
---------- 分隔线 ----------
---------- 分隔线 ----------
功能实现
代码、界面的介绍
具体代码:这里就不详细说 MagicIndicator 了
1、颜色值
<color name="color_indicator_selected">#FF111111</color>
<color name="color_indicator_normal">#FF666666</color>
<color name="color_read">#ff0000</color>
2、JudgeNestedScrollView
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.core.widget.NestedScrollView
import kotlin.math.abs
class JudgeNestedScrollView : NestedScrollView {
private var isNeedScroll = true
private var xDistance: Float = 0.toFloat()
private var yDistance: Float = 0.toFloat()
private var xLast: Float = 0.toFloat()
private var yLast: Float = 0.toFloat()
private var scaledTouchSlop: Int = 0
constructor(context: Context) : super(context, null) {}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, 0) {}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
yDistance = 0f
xDistance = yDistance
xLast = ev.x
yLast = ev.y
}
MotionEvent.ACTION_MOVE -> {
val curX = ev.x
val curY = ev.y
xDistance += abs(curX - xLast)
yDistance += abs(curY - yLast)
xLast = curX
yLast = curY
return !(xDistance >= yDistance || yDistance < scaledTouchSlop) && isNeedScroll
}
}
try {
return super.onInterceptTouchEvent(ev)
} catch (e: Exception) {
return false
}
}
/*
该方法用来处理NestedScrollView是否拦截滑动事件
*/
fun setNeedScroll(isNeedScroll: Boolean) {
this.isNeedScroll = isNeedScroll
}
}
3、ViewPagerNoScrollHorizontally
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.viewpager.widget.ViewPager
import kotlin.math.abs
class ViewPagerNoScrollHorizontally @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) :
ViewPager(context, attrs) {
private var downX = 0f
private var downY = 0f
var canScroll = false
override fun canScrollHorizontally(direction: Int): Boolean {
return canScroll
}
var isNestScrollChildEnable = true
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when(ev?.action){
MotionEvent.ACTION_DOWN -> {
downX = ev.x
downY = ev.y
}
MotionEvent.ACTION_MOVE -> {
val absDeltaX = abs(ev.x - downX)
val absDeltaY = abs(ev.y - downY)
if (absDeltaX > ViewConfiguration.get(context).scaledTouchSlop ||
absDeltaY > ViewConfiguration.get(context).scaledTouchSlop
) {
if (!isNestScrollChildEnable){
return true
}
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
downX = 0f
downY = 0f
}
}
return super.onInterceptTouchEvent(ev)
}
}
4、ScaleTransitionPagerTitleView 指示器用
import android.content.Context;
import net.lucode.hackware.magicindicator.buildins.commonnavigator.titles.ColorTransitionPagerTitleView;
public class ScaleTransitionPagerTitleView extends ColorTransitionPagerTitleView {
private float mMinScale = 0.85f;
public ScaleTransitionPagerTitleView(Context context) {
super(context);
}
@Override
public void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight) {
super.onEnter(index, totalCount, enterPercent, leftToRight); // 实现颜色渐变
setScaleX(mMinScale + (1.0f - mMinScale) * enterPercent);
setScaleY(mMinScale + (1.0f - mMinScale) * enterPercent);
}
@Override
public void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight) {
super.onLeave(index, totalCount, leavePercent, leftToRight); // 实现颜色渐变
setScaleX(1.0f + (mMinScale - 1.0f) * leavePercent);
setScaleY(1.0f + (mMinScale - 1.0f) * leavePercent);
}
public float getMinScale() {
return mMinScale;
}
public void setMinScale(float minScale) {
mMinScale = minScale;
}
}
5、MyViewPagerAdapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
class MyViewPagerAdapter(
private var list: MutableList<Fragment>,
fm: FragmentManager
) : FragmentPagerAdapter(fm){
override fun getItem(position: Int): Fragment {
return list[position]
}
override fun getCount(): Int {
return list.size
}
}
6、MyDemoListAdapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_my_demo.view.*
class MyDemoListAdapter(
var context: Context,
var list: MutableList<String>
) :
RecyclerView.Adapter<MyDemoListAdapter.ViewHolder>() {
private val layoutInflater = LayoutInflater.from(context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ViewHolder(
layoutInflater.inflate(
R.layout.item_my_demo,
parent,
false
)
)
override fun getItemCount(): Int {
return list.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindData(list[position], position)
}
//设置数据
fun setData(dataList: MutableList<String>) {
list.clear()
list.addAll(dataList)
notifyDataSetChanged()
}
inner class ViewHolder(var view: View) : RecyclerView.ViewHolder(view) {
fun bindData(str: String, position: Int) {
itemView.item_my_demo_tv.text = str
}
}
}
item_my_demo
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/item_my_demo_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:textSize="25dp" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="#55ff0000" />
</LinearLayout>
上面这写是准备用的控件、adapter等,下面开始使用
1、fragment_one
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--必须具备滑动属性,ScrollView把滑动事件分发下来,这里需要滑动-->
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/frag_one_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:lineSpacingExtra="3dp"
android:textSize="20dp" />
</androidx.core.widget.NestedScrollView>
</LinearLayout>
OneFragment
import androidx.fragment.app.Fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_one.*
class OneFragment : Fragment() {
//先于 onViewCreated 执行
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
var view: View = inflater.inflate(R.layout.fragment_one, container, false);
return view
}
//先于 onStart() 执行
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
init()
}
private fun init() {
val str: String =
"这是第一段内容:内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容\n\n" +
"这是第二段内容:内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容\n\n" +
"这是第三段内容:内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容\n\n" +
"这是第四段内容:内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容\n\n" +
"这是第五段内容:内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容"
frag_one_tv.text = str
}
}
2、fragment_two
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/my_demo_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:scrollbars="none" />
</LinearLayout>
TwoFragment
import androidx.fragment.app.Fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_two.*
class TwoFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
//先于 onViewCreated 执行
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
var view: View = inflater.inflate(R.layout.fragment_two, container, false);
return view
}
//先于 onStart() 执行
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
init()
}
private var myDemoListAdapter: MyDemoListAdapter? = null
private fun init() {
//初始化适配器
myDemoListAdapter = MyDemoListAdapter(activity!!, mutableListOf())
my_demo_rv?.apply {
layoutManager = LinearLayoutManager(activity)
adapter = myDemoListAdapter
}
//准备数据
val dataList: MutableList<String> = mutableListOf()
dataList.add("第 1 条数据")
dataList.add("第 2 条数据")
dataList.add("第 3 条数据")
dataList.add("第 4 条数据")
dataList.add("第 5 条数据")
dataList.add("第 6 条数据")
dataList.add("第 7 条数据")
dataList.add("第 8 条数据")
dataList.add("第 9 条数据")
dataList.add("第 10 条数据")
dataList.add("第 11 条数据")
dataList.add("第 12 条数据")
dataList.add("第 13 条数据")
dataList.add("第 14 条数据")
dataList.add("第 15 条数据")
//设置数据
myDemoListAdapter?.setData(dataList)
}
}
主界面
activity_main
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#33ff0000"
android:gravity="center"
android:text="这是固定不动的"
android:textSize="20dp" />
<com.demo.mydemo.JudgeNestedScrollView
android:id="@+id/page_jnsv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!--内容区域-->
<LinearLayout
android:id="@+id/content_ll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="这是内容介绍,这是内容介绍,这是内容介绍,这是内容介绍,这是内容介绍,这是内容介绍,这是内容介绍"
android:textSize="20dp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="5dp"
android:background="#5500ff00" />
<net.lucode.hackware.magicindicator.MagicIndicator
android:id="@+id/magic_indicator"
android:layout_width="match_parent"
android:layout_height="44dp" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#550000ff" />
<com.demo.mydemo.ViewPagerNoScrollHorizontally
android:id="@+id/act_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</com.demo.mydemo.JudgeNestedScrollView>
</LinearLayout>
</LinearLayout>
MainActivity
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.Gravity
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.activity_main.*
import net.lucode.hackware.magicindicator.ViewPagerHelper
import net.lucode.hackware.magicindicator.buildins.UIUtil
import net.lucode.hackware.magicindicator.buildins.commonnavigator.CommonNavigator
import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.CommonNavigatorAdapter
import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IPagerIndicator
import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IPagerTitleView
import net.lucode.hackware.magicindicator.buildins.commonnavigator.indicators.LinePagerIndicator
import net.lucode.hackware.magicindicator.buildins.commonnavigator.titles.badge.BadgePagerTitleView
class MainActivity : AppCompatActivity() {
private val tabList: MutableList<String> = mutableListOf("第一界面", "第二界面")
//ScrollView可滑动的距离
private var svTop: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initMagicIndicator()
initVP()
/**
* 注:
* ScrollView 和 子布局 的滑动事件是互斥的,即:A消费滑动时,B不可滑动
* 子布局一点要具备滑动的功能、属性(参考:fragment_one、fragment_two)
*/
page_jnsv.setOnScrollChangeListener { _: NestedScrollView?, _, _, _, _ ->
Log.e("监听滑动", "${page_jnsv.scrollY} ;svTop = $svTop")
page_jnsv.setNeedScroll(page_jnsv.scrollY < svTop)
//子布局 是否可以滑动
act_viewpager.isNestScrollChildEnable = page_jnsv.scrollY >= svTop
}
//最开始,默认 子布局 不可滑动
act_viewpager.isNestScrollChildEnable = false
ceLiang()
}
//测量
private fun ceLiang() {
content_ll.post {
/**
* 可滑动的距离:根据功能需求可知,为 内容介绍区域+绿色分割线
*
* svTop = 介绍TextView的高度 + 绿色分隔线高度
* 为了简化计算,避免过多的测量、加减,这里合理利用布局和代码,获取到 magic_indicator控件距离父控件顶部的距离
*/
svTop = magic_indicator.top
Log.e("svTop = ", "$svTop")
//viewpager的高度
var vpH = page_jnsv.measuredHeight - dip2px(
this,
44f
)
Log.e("viewpager的高度 = ", "$vpH")
//第一种高度设置方法
//viewpager的高度
act_viewpager.layoutParams.height = vpH
//这里建议重绘一下。避免某些情况下,viewpager的高度异常
act_viewpager.requestLayout()
//第二种高度设置方法
// val lp: LinearLayout.LayoutParams = act_viewpager.layoutParams as LinearLayout.LayoutParams
// lp.height = vpH
// act_viewpager.layoutParams=lp
}
}
private fun initMagicIndicator() {
val commonNavigator = CommonNavigator(this)
commonNavigator.isAdjustMode = true
commonNavigator.adapter = object : CommonNavigatorAdapter() {
override fun getCount(): Int {
return tabList.size
}
override fun getTitleView(context: Context, index: Int): IPagerTitleView {
val badgePagerTitleView = BadgePagerTitleView(context)
val simplePagerTitleView = ScaleTransitionPagerTitleView(context)
simplePagerTitleView.normalColor =
context.resources.getColor(R.color.color_indicator_normal)
simplePagerTitleView.selectedColor =
context.resources.getColor(R.color.color_indicator_selected)
simplePagerTitleView.textSize = 17f
simplePagerTitleView.text = tabList[index]
simplePagerTitleView.gravity = Gravity.CENTER
//点击tab切换
simplePagerTitleView.setOnClickListener {
act_viewpager.currentItem = index
}
badgePagerTitleView.innerPagerTitleView = simplePagerTitleView
return badgePagerTitleView
}
override fun getIndicator(context: Context): IPagerIndicator {
val linePagerIndicator = LinePagerIndicator(context)
linePagerIndicator.setColors(context.resources.getColor(R.color.color_read))
linePagerIndicator.lineHeight = UIUtil.dip2px(context, 2.0).toFloat()
linePagerIndicator.lineWidth = UIUtil.dip2px(context, 20.0).toFloat()
linePagerIndicator.roundRadius = UIUtil.dip2px(context, 1.5).toFloat()
linePagerIndicator.mode = LinePagerIndicator.MODE_EXACTLY
return linePagerIndicator
}
}
magic_indicator.navigator = commonNavigator
ViewPagerHelper.bind(magic_indicator, act_viewpager)
}
//初始化 viewpager 及填充数据
private fun initVP() {
val fragList: MutableList<Fragment> = mutableListOf()
fragList.add(OneFragment())
fragList.add(TwoFragment())
act_viewpager.adapter = MyViewPagerAdapter(fragList, supportFragmentManager)
}
private fun dip2px(context: Context, dpValue: Float): Int {
val scale = context.resources.displayMetrics.density
return (dpValue * scale + 0.5f).toInt()
}
}