自己定义ViewpagerIndicator (仿猫眼,加入边缘回弹滚动效果)
一.概述
今天主要来分享个自己定义viewpagerindicator。效果主要是仿 猫眼电影 顶部的栏目切换。也就是我们常说的indicator,难度简单,为了让滑动时效果更炫酷,我在滑动到左边第一个item或者最右边的item时,加入了滑动到边缘位置后,回弹然后复位的效果(事实上也是非常easy,仅仅要计算好距离就好啦)
大致的效果图就是这样。
大家能够凑合看看(能够看到当滑动到边缘位置的时候有回弹的效果,是不是挺带感的O(∩_∩)O)
二.用法
- layout布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:background="@color/red">
<mr_immortalz.com.viewpagerindicator.ViewPagerIndicator
android:id="@+id/indicator"
android:layout_width="200dp"
android:layout_height="36dp"></mr_immortalz.com.viewpagerindicator.ViewPagerIndicator>
</LinearLayout>
<android.support.v4.view.ViewPager
android:id="@+id/vp"
android:layout_width="match_parent"
android:layout_height="match_parent"></android.support.v4.view.ViewPager>
</LinearLayout>
2.MainActivity用法
public class MainActivity extends AppCompatActivity {
private ViewPager viewPager;
private ViewPagerIndicator indicator;
private FragmentPagerAdapter mAdapter;
private List<Fragment> mList;
private List<String> mDatas;
private int itemCount = 2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewPager = (ViewPager) findViewById(R.id.vp);
indicator = (ViewPagerIndicator) findViewById(R.id.indicator);
mList = new ArrayList<Fragment>();
for (int i = 0; i < itemCount; i++) {
Fragment fragment = new MeFragment();
mList.add(fragment);
}
mDatas = new ArrayList<>();
for (int i = 0; i < itemCount; i++) {
mDatas.add("i=" + i);
}
mAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) {
@Override
public Fragment getItem(int position) {
return mList.get(position);
}
@Override
public int getCount() {
return mList.size();
}
};
viewPager.setAdapter(mAdapter);
//将viewpager与indicator绑定
indicator.setDatas(mDatas);
indicator.setViewPager(viewPager);
}
}
3.自己定义ViewpagerIndicator
public class ViewPagerIndicator extends LinearLayout {
private ViewPager mViewPager;
private int width;
private int height;
private int visibleItemCount = 3;
private int itemCount = 3;
//绘制框框
private Paint paint;
private float mWidth = 0;
private float mHeight = 0;
private float mLeft = 0;
private float mTop = 0;
private float radiusX = 10;
private float radiusY = 10;
private int mPadding = 8;
private List<String> mDatas;
private boolean isSetData = false;
private Context context;
private int currentPosition;
private boolean isAutoSelect = false;//推断是否进行切换
private float rebounceOffset;
public ViewPagerIndicator(Context context) {
super(context);
this.context = context;
init();
}
public ViewPagerIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
public ViewPagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init();
}
private void init() {
this.setBackgroundDrawable(getResources().getDrawable(R.drawable.bg));
paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(getResources().getColor(R.color.white));
paint.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth();
height = getMeasuredHeight();
mWidth = width / visibleItemCount;
mHeight = height;
LogUtil.m("width " + width + " height " + height);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
LogUtil.m();
super.onSizeChanged(w, h, oldw, oldh);
if (isSetData) {
isSetData = false;
this.removeAllViews();
//加入TextView
for (int i = 0; i < mDatas.size(); i++) {
TextView tv = new TextView(context);
tv.setPadding(mPadding, mPadding, mPadding, mPadding);
tv.setText(mDatas.get(i));
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
lp.width = width / visibleItemCount;
lp.height = height;
tv.setGravity(Gravity.CENTER);
tv.setTextColor(getResources().getColor(R.color.font_red));
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
tv.setLayoutParams(lp);
final int finalI = i;
tv.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mViewPager != null) {
mViewPager.setCurrentItem(finalI);
}
}
});
this.addView(tv);
}
setTitleColor();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
}
@Override
protected void onDraw(Canvas canvas) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//drawRoundRect须要的最低API是21
canvas.drawRoundRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, radiusX, radiusY, paint);
} else {
canvas.drawRoundRect(new RectF(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding), radiusX, radiusX, paint);
//canvas.drawRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, paint);
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
//ogUtil.m();
super.dispatchDraw(canvas);
}
public void setViewPager(ViewPager viewpager, int position) {
this.mViewPager = viewpager;
this.currentPosition = position;
if (mViewPager != null) {
viewpager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//当移动的是最左边item
if (isAutoSelect && currentPosition == 0) {
//滑动手松开时,让最左边(即第一个)item滑动到左边缘位置
if (positionOffset > rebounceOffset / 2) {
mLeft = (position + (positionOffset - rebounceOffset / 2) * 2) * mWidth;
} else if (positionOffset > rebounceOffset / 3 && positionOffset < rebounceOffset / 2) {
//让最左边(即第一个)item 向右回弹一部分距离
mLeft = (position + (rebounceOffset / 2) - positionOffset) * mWidth * 6 / 12;
} else {
//让最左边(即最后一个)item 向左回弹到边缘位置
mLeft = (position + positionOffset) * mWidth * 6 / 12;
}
invalidate();
} else if (isAutoSelect && currentPosition == itemCount - 1) {
//当移动的是最右边(即最后一个)item
//滑动手松开时,让最右边(即最后一个)item滑动到右边缘位置
if (positionOffset >= rebounceOffset && positionOffset < (1 - (1 - rebounceOffset) / 2)) {
//
mLeft = (position + positionOffset / (1 - (1 - rebounceOffset) / 2)) * mWidth;
//当item数大于visibleItem可见数。本控件(本质LinearLayout)才滚动
if (visibleItemCount < itemCount) {
scrollTo((int) (mWidth * positionOffset / (1 - (1 - rebounceOffset) / 2) + (position - visibleItemCount + 1) * mWidth), 0);
}
if ((mLeft + mWidth) > (getChildCount() * mWidth)) {
//当(mLeft + mWidth)大于最边缘的宽度时,设置
mLeft = (itemCount - 1) * mWidth;
}
} else if (positionOffset > (1 - (1 - rebounceOffset) / 2) && positionOffset < (1 - (1 - rebounceOffset) / 4)) {
//让最右边(即最后一个)item 向左回弹一部分距离
//当item数大于visibleItem可见数,且本控件未滚动到指定位置,则设置控件滚动到指定位置
if (visibleItemCount < itemCount && getScrollX() != (itemCount - visibleItemCount) * mWidth) {
scrollTo((int) ((itemCount - visibleItemCount) * mWidth), 0);
}
mLeft = (position + 1) * mWidth - (positionOffset - (1 - (1 - rebounceOffset) / 2)) * mWidth * 7 / 12;
} else {
//让最右边(即最后一个)item 向右回弹到边缘位置
//由于onPageScrolled 最后positionOffset会变成0,所以这里须要推断一下
//当positionOffset = 0 时。设置mLeft位置
if (positionOffset != 0) {
mLeft = (position + 1) * mWidth - (1.0f - positionOffset) * mWidth * 7 / 12;
if (mLeft > (itemCount - 1) * mWidth) {
mLeft = (itemCount - 1) * mWidth;
}
} else {
mLeft = (itemCount - 1) * mWidth;
}
}
invalidate();
} else {
//当移动的是中间item
scrollTo(position, positionOffset);
rebounceOffset = positionOffset;
}
setTitleColor();
}
@Override
public void onPageSelected(int position) {
LogUtil.m("position " + position);
currentPosition = position;
}
@Override
public void onPageScrollStateChanged(int state) {
LogUtil.m("state " + state);
if (state == 2) {
//当state = 2时,表示手松开。viewpager自己主动滑动
isAutoSelect = true;
}
if (state == 0) {
//当state = 0时,表示viewpager滑动停止
isAutoSelect = false;
}
}
});
}
}
public void setViewPager(ViewPager viewpager) {
setViewPager(viewpager, 0);
}
/**
* 正常滑动
* @param position
* @param positionOffset
*/
private void scrollTo(int position, float positionOffset) {
if (visibleItemCount < itemCount) {
if (positionOffset > 0 && position > (visibleItemCount - 2)) {
this.scrollTo((int) (mWidth * positionOffset + (position - visibleItemCount + 1) * mWidth), 0);
}
}
mLeft = (position + positionOffset) * mWidth;
invalidate();
}
/**
* 设置字体颜色
*/
private void setTitleColor() {
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
if (i == currentPosition) {
((TextView) getChildAt(currentPosition)).setTextColor(getResources().getColor(R.color.font_red));
} else {
((TextView) getChildAt(i)).setTextColor(getResources().getColor(R.color.font_white));
}
}
}
}
/**
* 设置内容数据
*
* @param mDatas
*/
public void setDatas(List<String> mDatas) {
this.isSetData = true;
this.mDatas = mDatas;
this.itemCount = mDatas.size();
if (itemCount < visibleItemCount) {
visibleItemCount = itemCount;
}
}
}
三.代码分析
非常明显,核心代码在ViewPagerIndicator中。由于代码中已经对每一个函数方法给出了凝视。以下说下大体思路。
1.首先init()。onMeasure中对paint,width,height等不可缺少的数据进行获取。
2.由于整个indicator是继承自linearlayout,对于里面的文字展示,用textview来显示。由于不知道用户使用的时候究竟有多少个item。所以在setDatas()方法中对textview数目进行绑定。然后在onSizeChanged中动态生成须要的textview数目(isSetData用来控制是否绑定了数据。绑定了的话。须要将之前所有生成的所有清空)
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
LogUtil.m();
super.onSizeChanged(w, h, oldw, oldh);
if (isSetData) {
isSetData = false;
this.removeAllViews();
//加入TextView
for (int i = 0; i < mDatas.size(); i++) {
TextView tv = new TextView(context);
tv.setPadding(mPadding, mPadding, mPadding, mPadding);
tv.setText(mDatas.get(i));
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
lp.width = width / visibleItemCount;
lp.height = height;
tv.setGravity(Gravity.CENTER);
tv.setTextColor(getResources().getColor(R.color.font_red));
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
tv.setLayoutParams(lp);
final int finalI = i;
tv.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mViewPager != null) {
mViewPager.setCurrentItem(finalI);
}
}
});
this.addView(tv);
}
setTitleColor();
}
}
仅仅所以在onsizechanged中动态加入,是由于该方法会在ondraw前,onMeasure方法后回调,这样就保证我们能获取到须要的width,height。
3.Ok,如今获取到须要绘制的数目后接下来就是绘制白色背景框框啦。
protected void onDraw(Canvas canvas) {
LogUtil.m();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//drawRoundRect须要的最低API是21
canvas.drawRoundRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, radiusX, radiusY, paint);
} else {
canvas.drawRoundRect(new RectF(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding), radiusX, radiusX, paint);
//canvas.drawRect(mLeft + mPadding, mTop + mPadding, mLeft + mWidth - mPadding, mTop + mHeight - mPadding, paint);
}
}
非常好理解。不解释`(∩_∩)′
4.接下来,最最关键的就是setViewPager()这种方法。
为了方便理解,大家能够看看
onPageScrolled(页面滚动时回调)
onPageSelected(滑动松手后回调,在一个滑动流程中仅仅会回调一次)
onPageScrollStateChanged(在一个滑动流程中会回调三次。详细代表含义能够看图中标注)
这三个方法滑动时。详细回调顺序。
从第一个item向右滑动到第二个item
从第二个item滑动到第一个item(不管左滑还是右滑回调流程都一致)
知道了上面我们就应该非常好理解了。
在onPageSelected中记录currentPosition的值。
在onPageScrollStateChanged中推断何时松开手,方便后面在松开手会对滑动进行处理
在onPageScrolled中进行滑动处理。
以下在详细说说onPageScrolled。
onPageScrolled中也有三个推断
1.处于最左边item且手滑动松开
2.处于最右边item且手滑动松开
3.其它item不管手是否滑动松开(这里用rebounceOffset记录手松开时,已经拖动的比例positionOffset)
else {
//当移动的是中间item
scrollTo(position, positionOffset);
rebounceOffset = positionOffset;
}
private void scrollTo(int position, float positionOffset) {
//item数量大于可见item,linearlayout才滑动
if (visibleItemCount < itemCount) {
if (positionOffset > 0 && position > (visibleItemCount - 2)) {
this.scrollTo((int) (mWidth * positionOffset + (position - visibleItemCount + 1) * mWidth), 0);
}
}
mLeft = (position + positionOffset) * mWidth;
invalidate();
}
分析第一种情况。
为了实现回弹。
在松手后的(positionOffset-0 ) 的时间段呢。分成三部分
看图
if (isAutoSelect && currentPosition == 0) {
//滑动手松开时,让最左边(即第一个)item滑动到左边缘位置
if (positionOffset > rebounceOffset / 2) {
mLeft = (position + (positionOffset - rebounceOffset / 2) * 2) * mWidth;
} else if (positionOffset > rebounceOffset / 3 && positionOffset < rebounceOffset / 2) {
//让最左边(即第一个)item 向右回弹一部分距离
mLeft = (position + (rebounceOffset / 2) - positionOffset) * mWidth * 6 / 12;
} else {
//让最左边(即最后一个)item 向左回弹到边缘位置
mLeft = (position + positionOffset) * mWidth * 6 / 12;
}
invalidate();
}
分析另外一种情况(剩余时间(positionOffset - 1 )也是分成了三部分。一部分回到边缘,一部分偏移。一部分用于复位。与第一种情况类似,不再贴图),当item滑向最有边缘时,与第一种情况不同的是,Linearlayout是须要向左移动的。所以liearlayout向左移动了X,我们绘制的白色边框须要向右移动X。才干保证。视觉上看起来白色边框没有动,动的是。我们的Linearlayout(不知道大家能理解不,可能我说的有点不太好理解,用纸好好绘制下简单理解些`(∩_∩)′)
else if (isAutoSelect && currentPosition == itemCount - 1) {
//当移动的是最右边(即最后一个)item
//滑动手松开时。让最右边(即最后一个)item滑动到右边缘位置
if (positionOffset >= rebounceOffset && positionOffset < (1 - (1 - rebounceOffset) / 2)) {
//
mLeft = (position + positionOffset / (1 - (1 - rebounceOffset) / 2)) * mWidth;
//当item数大于visibleItem可见数。本控件(本质LinearLayout)才滚动
if (visibleItemCount < itemCount) {
scrollTo((int) (mWidth * positionOffset / (1 - (1 - rebounceOffset) / 2) + (position - visibleItemCount + 1) * mWidth), 0);
}
if ((mLeft + mWidth) > (getChildCount() * mWidth)) {
//当(mLeft + mWidth)大于最边缘的宽度时,设置
mLeft = (itemCount - 1) * mWidth;
}
} else if (positionOffset > (1 - (1 - rebounceOffset) / 2) && positionOffset < (1 - (1 - rebounceOffset) / 4)) {
//让最右边(即最后一个)item 向左回弹一部分距离
//当item数大于visibleItem可见数。且本控件未滚动到指定位置。则设置控件滚动到指定位置
if (visibleItemCount < itemCount && getScrollX() != (itemCount - visibleItemCount) * mWidth) {
scrollTo((int) ((itemCount - visibleItemCount) * mWidth), 0);
}
mLeft = (position + 1) * mWidth - (positionOffset - (1 - (1 - rebounceOffset) / 2)) * mWidth * 7 / 12;
}
OK。三种情况都分析完成。
最后我们的控件也算是大功告成啦`(∩_∩)′
源代码下载地址 https://github.com/ImmortalZ/ViewPagerIndicator
欢迎star。fork!`(∩_∩)′
自己定义ViewpagerIndicator (仿猫眼,加入边缘回弹滚动效果)的更多相关文章
- iOS仿UC浏览器顶部频道滚动效果
很喜欢用UC浏览器上网,当然不是给UC打广告,里面有很多酷炫的效果,值的学习,这次分享的是频道滚动的效果.动画效果如下: 实现的这个效果的关键是绘制,重写顶部Label的drawRect方法 gith ...
- 模仿猫眼电影App一个动画效果
看真正的猫眼效果图 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMzIxMDYyMA==/font/5a6L5L2T/fontsize/400/f ...
- Android中仿淘宝首页顶部滚动自定义HorizontalScrollView定时水平自动切换图片
Android中仿淘宝首页顶部滚动自定义HorizontalScrollView定时水平自动切换图片 自定义ADPager 自定义水平滚动的ScrollView效仿ViewPager 当遇到要在Vie ...
- jquery仿淘宝规格颜色选择效果
jquery实现的仿淘宝规格颜色选择效果源代码如下 jquery仿淘宝规格颜色选择效果 -收缩HTML代码 运行代码 [如果运行无效果,请自行将源代码保存为html文件运行] <script t ...
- 它们的定义TextView使之具有跑马灯的效果
一.引入问题 使用通用textview快乐效应,焦点事件不启动滚动,button目前的焦点事件,但丑,因此,需要定制TextView 天生焦点 个textview FocusedTextView.ja ...
- JS仿QQ空间鼠标停在长图片时候图片自动上下滚动效果
JS仿QQ空间鼠标停在长图片时候图片自动上下滚动效果 今天是2014年第一篇博客是关于类似于我们的qq空间长图片展示效果,因为一张很长的图片不可能全部把他展示出来,所以外层用了一个容器给他一个高度,超 ...
- JS 仿腾讯发表微博的效果
JS 仿腾讯发表微博的效果 最近2天研究了下 腾讯发表微博的效果 特此来分享下,效果如下: 在此分享前 来谈谈本人编写代码的习惯,很多人会问我既然用的是jquery框架 为什么写的组件不用Jquery ...
- jQuery练手:仿新浪微博图片文字列表淡进淡出上下滚动效果
1.效果及功能说明 仿新浪微博图片文字列表上下淡进淡出间歇上下滚动 2.实现原理 首先要设定div内只能显示4个图片那么多出来的图片会自动隐藏然后在给图片添加一个动画的事件让他们可以滚动的播放出来上下 ...
- iOS仿支付宝首页的刷新布局效果
代码地址如下:http://www.demodashi.com/demo/12753.html XYAlipayRefreshDemo 运行效果 动画效果分析 1.UI需要变动,向上滑动的时候,顶部部 ...
随机推荐
- SpringBoot 2.x (1):手动创建项目与自动创建项目
SpringBoot 2.x基于Spring Framework 5.x 环境需求如下: JDK1.8或以上 Maven3.2或以上 这里我使用的是Eclipse,IDEA这个工具很强大,但不习惯它 ...
- Chromium浏览器编译成功庆祝
1.什么是Chromium Chromium 是Google公司的开源项目 Google浏览器 最新版360浏览器 都是在Chromium的基础上重新编译的. 2.什么是双核浏览器 ...
- CommHelper
18位流水号: public static string GenerateTransId(int i) { string transId = DateTime.Now.ToString("y ...
- Navicat Premium 12 破解方法
基本安装下一步下一步,破解方法参考:地址
- oracle数据库跨库查询
create public database link mylink connect to orclname identified by orclpasswd using 'ORCL'; drop p ...
- Squid 正向代理
实现通过特定设备对特定的网址访问加速 使用squid 正向代理 实现,区别于反向代理,两者区别的根本在于作为中转的服务器在一个完整的请求中是代表客户端还是代表服务器. 服务端设置 1.安装程序包(推荐 ...
- axios方法封装
axios方法封装 一般情况下,我们会用到的方法有:GET,POST,PUT,PATCH,封装方法如下: 五.封装后的方法的使用 1.在main.js文件里引用之前写好的文件,我的命名为htt ...
- Description Resource Path Location Type Missing artifact com.********:framework:jar:1.0.2 pom.xml /项目名 line **** Maven Dependency Problem
问题具体描述如下图所示: 对于该问题本人是这么解决的. 在window下[Preferences]目录找到[Maven]下的[usersetting] 查看local repository 里面的路径 ...
- JAVA基础——集合类汇总
一.集合与数组 数组(可以存储基本数据类型)是用来存现对象的一种容器,但是数组的长度固定,不适合在对象数量未知的情况下使用. 集合(只能存储对象,对象类型可以不一样)的长度可变,可在多数情况下使用. ...
- Vue.js 计算属性(computed)
Vue.js 计算属性(computed) 如果在模板中使用一些复杂的表达式,会让模板显得过于繁重,且后期难以维护.对此,vue.js 提供了计算属性(computed),你可以把这些复杂的表达式写到 ...