自定义View之仿今日头条颜色渐变指示器导航栏
2017-06-07 19:05
357 查看
自定义View之仿今日头条颜色渐变指示器导航栏
前言
本文是自定义view的练习,默认读者掌握了自定义view的知识本文是对本人上一篇写的控件《自定义view之歌词渐变文本控件》lyricTextView的封装应用。
源码地址:https://github.com/CCY0122/lyricindicator
效果图
与今日头条(v6.1.1)的部分区别:
1、选中的文字不会被放大(今日头条会放大一丢丢)
2、当item数超出屏幕时,滚动时机不同
使用方法
源码很少,建议直接复制即可(LyricIndicator.class 、LyricTextView.class、attrs.xml)第一步,xml里引入
<com.example.lyricindicator.LyricIndicator android:id="@+id/indicator" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#11000000" app:item_padding="7dp" app:text_size="20sp" app:default_color="#000000" app:changed_color="#ff0000"> </com.example.lyricindicator.LyricIndicator>
可使用的属性有:
text_size 字体大小
default_color默认颜色
changed_color渐变颜色
字体的左右上下padding:
item_padding_l
item_padding_r
item_padding_t
item_padding_b
item_padding
注意:IDE可能还会列出text、progress、direction这些属性,这些属性属于lyricTextView,设置了也是无效的。
第二步:与viewpager进行关联:
lyricIndicator = (LyricIndicator) findViewById(R.id.indicator); lyricIndicator.setupWithViewPager(mViewPager);
注意:ViewPager的adapter要实现
public CharSequence getPageTitle(int position)作为每一页对应的title
实现
第一步
首先,要学习lyricTextView。第二步
当然是在attrs里为我们的控件定义一些属性,贴上attrs:<resources> <attr name="text_size" format="dimension" /> <attr name="default_color" format="color|reference" /> <attr name="changed_color" format="color|reference" /> <declare-styleable name="LyricTextView"> <attr name="text" format="string" /> <attr name="text_size" /> <attr name="default_color"/> <attr name="changed_color"/> <attr name="progress" format="float" /> <attr name="direction"> <enum name="left" value="0" /> <enum name="right" value="1" /> </attr> </declare-styleable> <declare-styleable name="LyricIndicator"> <attr name="text_size"/> <attr name="default_color"/> <attr name="changed_color"/> <attr name="item_padding_l" format="dimension"/> <attr name="item_padding_r" format="dimension"/> <attr name="item_padding_t" format="dimension"/> <attr name="item_padding_b" format="dimension"/> <attr name="item_padding" format="dimension"/> </declare-styleable> </resources>
<declare-styleable name="LyricTextView"></>里的是lyricTextView 的属性。
<declare-styleable name="LyricIndicator"></>里的是本控件的属性,这里注意,text_size、default_color、changed_color是这两个控件都有的,相同属性不允许重复定义,所以我们要提出来在开头就定义,否则报错。这些属性作用应该看下名字都能理解。
然后呢,创建LyricIndicator继承自HorizontalScrollView。实现前三个构造,构造方法里初始化属性:
public LyricIndicator(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.context = context; TypedArray t = context.obtainStyledAttributes(attrs, R.styleable.LyricIndicator); textSize = t.getDimension(R.styleable.LyricIndicator_text_size, sp2px(14)); defaultColor = t.getColor(R.styleable.LyricIndicator_default_color, DEFAULT_COLOR); changeColor = t.getColor(R.styleable.LyricIndicator_changed_color, CHANGED_COLOR); padding = (int) t.getDimension(R.styleable.LyricIndicator_item_padding, 0); paddingL = (int) t.getDimension(R.styleable.LyricIndicator_item_padding_l, padding); paddingR = (int) t.getDimension(R.styleable.LyricIndicator_item_padding_r, padding); paddingT = (int) t.getDimension(R.styleable.LyricIndicator_item_padding_t, padding); paddingB = (int) t.getDimension(R.styleable.LyricIndicator_item_padding_b, padding); t.recycle(); addBaseView(context); }
可以看到设置好初始化属性后,还调用了
addBaseView(context)。我们的控件是继承自HorizontalScrollView的,它的内部应只有一个子布局,那我们就放一个方向为水平的LinearLayout,然后之后添加的的item(即LyricTextView)都放在这个LinearLayout里。
private void addBaseView(Context context) { baseLinearLayout = new LinearLayout(context); baseLinearLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); baseLinearLayout.setOrientation(LinearLayout.HORIZONTAL); baseLinearLayout.setGravity(Gravity.CENTER_VERTICAL); addView(baseLinearLayout); }
第三步
通过关联viewPager来完成控件初始化。关联viewPager后,我们的控件就与它进行了绑定。首先,我们要根据viewPager的页数来生成对应数量的item,并监听viewPager的滚动事件,监听item们的点击事件。关联代码如下:
/** * 关联viewpager, * @param vp */ public void setupWithViewPager(final ViewPager vp) { this.vp = vp; if ( vp == null || vp.getAdapter() == null) { return; } addLyricTextViews(); addClickEvent(); vp.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { itemScroll(position, positionOffset); } @Override public void onPageSelected(int position) { Log.d("ccy", "onPageSelected" + position); resetAllItem(); } @Override public void onPageScrollStateChanged(int state) { if(state == ViewPager.SCROLL_STATE_IDLE){ //解决残影,不够完美 resetAllItem(); } } }); }
该方法中,我们获取到viewPager之后,首先调用了
addLyricTextViews()来生成对应每一页的item,然后调用
addClickEvent()为item们添加点击事件,然后监听了viewpager的滚动事件,在滚动时,即在 onPageScrolled回调里,我们通过
itemScroll(position, positionOffset)来进行两个item之间的颜色渐变,即当前的item的进度progress要从1 –> 0,并且方向direction设置为右,而即将选中的item的进度progress要从0 –> 1,并且方向direction设置为左。
onPageScrolled回调中的参数说明:假设当前选中的item是2,如果当前滑动方向是从左往右时,position为2,positionOffset为[0,1)中的一个值,也即滑动的比例,并且是从0慢慢增加到1;如果当前滑动方向是从右往左,那么position的值就为1了(虽然当前选中position为2),positionOffset是从1慢慢减少到0。
下面看下
addLyricTextViews()方法:
/** * 添加所有item */ private void addLyricTextViews() { currentPos = vp.getCurrentItem(); for (int i = 0; i < vp.getAdapter().getCount(); i++) { LyricTextView ltv = new LyricTextView(context); ltv.setAll(0f, vp.getAdapter().getPageTitle(i)+"", textSize, defaultColor, changeColor, LyricTextView.LEFT); ltv.setPadding(paddingL, paddingT, paddingR, paddingB); ltv.setTag(i); baseLinearLayout.addView(ltv); if (i == currentPos) { ltv.setProgress(1); } } }
根据
vp.getAdapter().getCount()获取到数量,然后初始化对应数量的LyricTextView,并添加到父布局baseLinearLayout里,将当前选中的item的progress设为1。
这里LyricTextView里的text是从
vp.getAdapter().getPageTitle(i)里获取而来的,一次我们写viewPager的adapter的时候记得要重写这个方法。
接下来看
addClickEvent():
private void addClickEvent() { for (int i = 0; i < baseLinearLayout.getChildCount(); i++) { LyricTextView ltv = (LyricTextView) baseLinearLayout.getChildAt(i); ltv.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { int pos = (int) v.getTag(); vp.setCurrentItem(pos); } }); } }
点击后根据之前存好的tag选中对应item,不用说什么了。
接下来看 onPageScrolled回调里的
itemScroll(position, positionOffset)
private void itemScroll(int position, float positionOffset) { if (positionOffset > 0 && position + 1 <= vp.getAdapter().getCount()) { LyricTextView left = (LyricTextView) baseLinearLayout.getChildAt(position); LyricTextView right = (LyricTextView) baseLinearLayout.getChildAt(position + 1); left.setDirection(LyricTextView.RIGHT); left.setProgress(1 - positionOffset); right.setDirection(LyricTextView.LEFT); right.setProgress(positionOffset); invalidate(); layoutScroll(position, positionOffset); } }
首先获取到滚动过程中涉及到的两个item,坐边的叫left,右边的叫right。
之前已经解释过了int position、float positionOffset这两个参数。我再啰嗦一下:当从左往右滑,那么left即当前选中的item,right是即将选中的item;当从右往左滑,left是即将要选中的item,right是当前选中的item。
如果你理解了,那么之后他俩setDirection和setProgress里填的值也肯定就理解了。然后记得invalidate。
然后呢,还调用了一个方法
layoutScroll(position, positionOffset);这个方法就是当item数总长度超过控件宽度时,后面的item总要在某个时刻滑出来的吧。今日头条app(v6.1.1)里滑动时机是当前item为最后一个或第一个完整可见的item时,才开始滑动(听不懂?打开今日头条看看新闻去吧)而我们的控件滑动时机是当前选中item在控件中心时开始滑动(听不懂?看效果图)。
layoutScroll代码:
private void layoutScroll(int pos, float positionOffset) { // Log.d("ccy","scroll x = " + calculateScrollXForTab(pos, positionOffset)); scrollTo(calculateScrollXForTab(pos, positionOffset), 0); } private int calculateScrollXForTab(int pos, float positionOffset) { LyricTextView selectedChild = (LyricTextView) baseLinearLayout.getChildAt(pos); LyricTextView nextChild = (LyricTextView) baseLinearLayout.getChildAt(pos + 1); final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0; final int nextWidth = nextChild != null ? nextChild.getWidth() : 0; // base scroll amount: places center of tab in center of parent int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2); // offset amount: fraction of the distance between centers of tabs int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset); return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) ? scrollBase + scrollOffset : scrollBase - scrollOffset; }
这个时候有人要吐槽了,为什么不做的跟今日头条一样呢?哈哈哈哈哈哈哈哈哈大学读了四年数学已废。。。。试着写了好几次都没写出对应滑动距离的计算公式来。。。。
所以我只好查看了TabLayout的源码(还是读源码叼),把calculateScrollXForTab拿来用了~~~~大家好好读一读calculateScrollXForTab,好好理解,就是计算scroll的距离,这个文字解释好麻烦。另外,读完后我还学到了原来ViewCompat.getLayoutDirection(this)可以判断当前滑动方向的(你早知道了?好吧……)。
好了,主体算是完成了,测试一下,滑动viewPager,恩,LyricIndicator也跟着滑动了,这个没问题。那直接点击某个item呢,咦,虽然能选中,但是之前的item居然留下了一点残影
上图是原本选中的是“111”,然后我点击了“asdasdasd”之后的效果图,可以看到111居然还有一点点是红色的。
这是为什么呢,根据我自己的排查,我认为原因是这样的:
OnPageChangeListener里onPageScrolled这个方法呢是在滑动过程中不断回调的,positionOffset的值是[0,1)之间,那么一次正常滑动的话可能最后一次调用onPageScrolled时positionOffset的值已经是0.99等非常接近1的值,但是如果滑动速度比较快(我们通过点击选中一个item,viewPager会快速滑动过去),最后一次的positionOffset值可能只有0.95等不那么接近1的值,这就导致了上一个item的留下了0.5的progress,也就是上图“111”留下的一点点红色。
咋解决的,先写这么个方法:
private void resetAllItem() { for (int i = 0; i < baseLinearLayout.getChildCount(); i++) { LyricTextView ltv = (LyricTextView) baseLinearLayout.getChildAt(i); if (i == vp.getCurrentItem()) { ltv.setProgress(1f); } else { ltv.setProgress(0f); } } invalidate(); }
在每次滑动结束后调用一次这个方法,就能解决残影了。那在哪里调用呢?
第一个想到的是
onPageSelected里,但是其实很多情景下
onPageSelected并不是在
onPageScrolled调用结束后才调用的,有时候会先与
onPageScrolled调用。所以我还在
onPageScrollStateChanged(int state)方法里判断了当前状态,当状态是不在滑动时,即
state == ViewPager.SCROLL_STATE_IDLE时也调用了一次该方法。
残影问题就解决的,但是解决的不够优雅。
总结
本自定义view是继承了HorizontalScrollView ,经过反思,其实继承TabLayout会是更好的选择,坑也会少些。。毕竟练手作品,大家看看就好另外,大家如果要动态设置一些属性的话,请自行添加setter/getter,别忘了setter里调用invalidate()重绘。
2017-06-16更新
小小更新下。1、今日头条指示器的滑动时机大概在指示器的宽度的0.8~0.9左右比例处,我的指示器的滑动时机是正中心(宽度的0.5处)。上面代码中
private int calculateScrollXForTab(int pos, float positionOffset)这个方法里面,有个值是这样的
int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);
我们可以稍作修改:
int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth()*PIVOT_X);
其中PIVOT_X代表一个比例值(0~1),想跟今日头条一样的话就赋值为0.8左右就可以啦~~大家可以赋多种值试试效果。
2、推荐大家去学习MagicIndicator 这个指示器的库,内置了很多效果,也很方便扩展自定义,比我这练手作品不知道强到哪去了,而且学完后让我体会到了面向接口编程的重要性!这个库的作者的博客里有对这个库的解析文章,鸿洋大大也推荐过,推荐大家学习。
相关文章推荐
- 【Android - 自定义View】之自定义颜色渐变的Tab导航栏
- 安卓自定义View——网易颜色渐变效果指示器
- Android自定义Viewpager指示器PagerIndicator-仿微博头条效果
- Android应用中仿今日头条App制作ViewPager指示器
- Android实现仿网易今日头条等自定义频道listview 或者grideview等item上移到另一个view中
- 自定义View之GradualView文字渐变-颜色渐变-图像渐变
- 使用自定义View直接显示今日头条头布局
- Android自定义ViewPagerIndicator实现炫酷导航栏指示器(ViewPager+Fragment)
- 自定义颜色、大小渐变的圆点指示器
- Android-导航栏特效-新闻类APP(仿iOS版网易新闻今日头条的文字渐变缩放特效)
- 安卓中自定义view控件代替radiogroup实现颜色渐变效果的写法
- 自定义View圆圈进度条,颜色渐变
- 高仿今日头条字体渐变指示器,滑动+点击切换,如丝顺滑
- 仿微信Tab颜色渐变自定义View
- Android 自定义的颜色滑动转换ViewPager指示器 ColorTransformIndicator
- android——仿网易今日头条等自定义频道listview 或者grideview等item上移到另一个view中
- 自定义View之颜色渐变折线图
- 自定义view:类似今日头条的类别选择功能
- TableView下拉表头放大 导航栏颜色透明度随着TableView偏移量渐变
- Android-导航栏特效-新闻类APP(仿iOS版网易新闻今日头条的文字渐变缩放特效)