一. 在最近的项目中需要实现一个这样的自定义View,有点类似一个开关选择按钮,可以点击、可以滑动。在按钮下方是一个ViewPager,根据按钮的切换,同时切换ViewPager的界面。同时滑动ViewPager,也会切换到按钮不同的登录方式。把实现步骤记录一下,方便以后扩展和复用,同时加深一下对自定义View的理解。
总结:主要解决的问题是 1. 自定义View的步骤 , 2. 点击和滑动的冲突 2. 自定义控件和ViewPager的联动切换。
问题: 想实现点击的时候滑块也可以向滑动按钮一样平滑的滑动到另一边,并且让下面的ViewPager也匀速滑动,有什么好的方法处理
二. 看下面的几个gif图的演示效果。
- 滑动上面的控件,让ViewPager中的Fragment进行切换。
滑动控件下方的ViewPager让按钮的滑块进行同向移动(gif文件过大,无法上传)
点击控件,切换不同的按钮背景
三. 实现上面三个效果的步骤:
1. 新建一个SwitchButton类继承自View, 实现View的构造方法,测量,绘制的方法,并在xml中引用该控件。
构造方法:
public SwitchButton(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView();
}
initView方法初始化需要绘制的Bitmap和画笔、以及绘制的一些位置参数
login_select_liner_bg是整个控件的大的背景图片
要滑动的选择器login_selected_text_bg
通过BitmapFactory.decodeResource把drawable文件夹下的png图片转换成Bitmap图像。
backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.login_select_liner_bg);
slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.login_selected_text_bg);
初始化绘制的时候是:slidingBitmap 在 backgroundBitmap的靠左居中, 让slidingBitmap 距离top为slidingMarginTop 。
slidingMarginTop = (backgroundBitmap.getHeight() - slidingBitmap.getHeight()) /2;
上面Bitmap的绘制需要画笔,初始化一个画笔
mBitMapPaint = new Paint();
mBitMapPaint.setAntiAlias(true);
控件上的文字也是绘制上去的,同样需要画笔,黄色文字和白色文字的画笔设置。
mDefaultTextPaint = new Paint();
mDefaultTextPaint.setAntiAlias(true);
mDefaultTextPaint.setTextSize(32);
mDefaultTextPaint.setStyle(Paint.Style.FILL);
mDefaultTextPaint.setTextAlign(Paint.Align.LEFT);
mDefaultTextPaint.setColor(Color.WHITE);
mSelectTextPaint = new Paint();
mSelectTextPaint.setAntiAlias(true);
mSelectTextPaint.setTextSize(32);
mSelectTextPaint.setStyle(Paint.Style.FILL);
mSelectTextPaint.setTextAlign(Paint.Align.LEFT);
mSelectTextPaint.setColor(getResources().getColor(R.color.orange));
- 初始化参数之后,开始进行测量:这是一个view, 设置自定义View的宽高为backgroundBitmap的宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
}
- 开始绘制:
slidingMarginLeft 表示滑动选择块离这个自定义view的左边偏移距离
passwordLoginStr =“密码登录”
smsLoginStr = “短信登录”
canvas.drawBitmap(backgroundBitmap,0 ,0, mBitMapPaint);
canvas.drawBitmap(slidingBitmap,slidingMarginLeft,slidingMarginTop, mBitMapPaint);
if (isSelectLeft) {
canvas.drawText(passwordLoginStr, textLeftMargin, textVerticalLocation, mSelectTextPaint);
canvas.drawText(smsLoginStr, slidMarginLeftMax + textLeftMargin, textVerticalLocation, mDefaultTextPaint);
} else {
canvas.drawText(passwordLoginStr, textLeftMargin, textVerticalLocation, mDefaultTextPaint);
canvas.drawText(smsLoginStr, slidMarginLeftMax + textLeftMargin, textVerticalLocation, mSelectTextPaint);
}
- 添加点击事件: 绘制出了view只后,只是如第一张图片一样是一个静态的效果,需要添加点击事件。
让View继承自View.OnClickListener 实现onClick方法:
默认当滑到左边的时候slidingBitmap 距离左边为5个像素(把像素装换成dp会有更好的适配效果),滑到右边的时候距离左边最大的距离slidMarginLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth() - 5(把像素装换成dp会有更好的适配)
isSelectLeft 表示是否点击选中左边的密码登录,默认为true,每点击一次进行切换,上面的文字绘制需要根据这个值进行判断。
//实现控件的点击事件
@Override
public void onClick(View v) {
isSelectLeft = !isSelectLeft;
if (isSelectLeft) {
slidingMarginLeft = 5;
} else {
slidingMarginLeft = slidMarginLeftMax;
}
//isChangeComplete = true;
invalidate();
}
在 initView 中 加上setOnClickListener(this);
这样就能相应点击事件了。
- 除了点击效果外,还需要添加滑动处理:实现onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//计算按下的坐标
isStartX = startX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float distanceX = endX - startX;
slidingMarginLeft += distanceX;
//防止将滑块滑出界面,屏蔽非法值
if(slidingMarginLeft > slidMarginLeftMax){
slidingMarginLeft = slidMarginLeftMax;
}else if(slidingMarginLeft < 5){
slidingMarginLeft = 5;
}
invalidate();
//数据还原
startX = event.getX();
break;
case MotionEvent.ACTION_UP:
if (slidingMarginLeft > slidMarginLeftMax / 2) {
slidingMarginLeft = slidMarginLeftMax;
isSelectLeft = false;
} else {
slidingMarginLeft = 5;
isSelectLeft = true;
}
invalidate();
break;
}
return true;
}
- 在添加了点击和触摸滑动效果之后会出现冲突效果,解决冲突的办法是,声明一个boolean值的变量isEnableClick,如果boolean 为true表示点击事件生效,当活动触摸是,将其设置为false, 表示点击事件不生效。
修改之后的点击事件onClick方法:
@Override
public void onClick(View v) {
if(isEnableClick) {
isSelectLeft = !isSelectLeft;
if (isSelectLeft) {
slidingMarginLeft = 5;
} else {
slidingMarginLeft = slidMarginLeftMax;
}
//isChangeComplete = true;
invalidate();
}
}
触摸事件 onTouchEvent也做相应的修改。
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//计算按下的坐标
isStartX = startX = event.getX();
isEnableClick = true;
break;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float distanceX = endX - startX;
slidingMarginLeft += distanceX;
//屏蔽非法值
if(slidingMarginLeft > slidMarginLeftMax){
slidingMarginLeft = slidMarginLeftMax;
}else if(slidingMarginLeft < 5){
slidingMarginLeft = 5;
}
//isChangeComplete = false;
invalidate();
//数据还原
startX = event.getX();
if(Math.abs(endX - isStartX) > 5){
//已经滑动了 就不再执行点击事件
isEnableClick = false;
}
break;
case MotionEvent.ACTION_UP:
if(!isEnableClick) {
if (slidingMarginLeft > slidMarginLeftMax / 2) {
slidingMarginLeft = slidMarginLeftMax;
isSelectLeft = false;
} else {
slidingMarginLeft = 5;
isSelectLeft = true;
}
invalidate();
}
break;
}
return true;
}
- 实现与ViewPager的联动方式一:点击按钮切换ViewPager
在View中第一个点击监听的interface
public interface ChangeSelectListener{
void changeSelect(boolean isSelectLeft);
}
public void setChangeSelectListener(ChangeSelectListener changeSelectListener){
mChangeSelectListener = changeSelectListener;
}
在onClick方法中加上回调方法
if(mChangeSelectListener != null && isEnableClick) {
mChangeSelectListener.changeSelect(isSelectLeft);
}
- XXActivity的xml中定义如下
<LinearLayout
android:id="@+id/login_model_switch_liner"
android:layout_width="wrap_content"
android:orientation="horizontal"
android:layout_height="0dp"
android:layout_weight="1.5"
android:gravity="center_vertical">
<kap.com.smarthome.android.ui.view.SwitchButton
android:id="@+id/switchButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/margin_20"
/>
</LinearLayout>
<android.support.v4.view.ViewPager
android:id="@+id/login_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="15dp"
android:layout_weight="9.5">
</android.support.v4.view.ViewPager>
在 XXActivity中ViewPager的处理代码:
//设置ViewPager的适配器, 设置展示的fragment
mMethodViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments.size();
}
});
//给ViewPager添加切换的监听器
mMethodViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//滑动ViewPager的时候让mSwitchButton的滑块跟着按照比例跟着滑动, //setSlidingMarginLeft的方法下面给出
mSwitchButton.setSlidingMarginLeft(positionOffset);
}
@Override
public void onPageSelected(int position) {
//当滑动结束之后,设置是滑动到左边还是右半边
//SwitchButton中的setSelectLeft方法后面给出
if(position == 0){
mSwitchButton.setSelectLeft(true);
}else{
mSwitchButton.setSelectLeft(false);
}
}
@Override
public void onPageScrollStateChanged(int state) {}
});
mListener = new SwitchButton.ChangeSelectListener() {
@Override
public void changeSelect(boolean isSelectLeft) {
if(isSelectLeft){
mMethodViewPager.setCurrentItem(0);
}else{
mMethodViewPager.setCurrentItem(1);
}
}
};
mSwitchButton.setChangeSelectListener(mListener);
- SwitchButton类中的setSlidingMarginLeft方法 传入的scalex是viewpager的滑动比例,根据这个值可以计算出当前slidingMarginLeft 的值
public void setSlidingMarginLeft(float scaleX){
if(scaleX != 0) {
slidingMarginLeft = (int) (slidMarginLeftMax * scaleX);
invalidate();
}
}
- SwitchButton类中的setSelectLeft方法, 表示ViewPager滑动结束,slidingMarginLeft 重新设置最终值
public void setSelectLeft(boolean selectLeft) {
isSelectLeft = selectLeft;
if (isSelectLeft) {
slidingMarginLeft = 5;
} else {
slidingMarginLeft = slidMarginLeftMax;
}
invalidate();
}
下面给出SwitchButton类的完整代码, ViewPager的代码在上面第8部分已经基本给出。
/**
* Created by Administrator on 2017/9/22 0022.
*
* 创建自定义View用于登录界面中密码登录和短信登录的切换
*
* 1.构造方法,实例化类
* 2.开始测量 measure(int , int) -> onMeasure();
* 如果当前view是一个ViewGroup,还有义务测量自己的孩子,孩子有建议权
* 3. 指定位置-layout()--> onlayout();
* 指定控件的位置,一般view不用写这个方法,ViewGroup的时候才需要,一般View不需要重写该方法
* 4. 绘制视图 -- draw() --> onDraw(Canvas)
* 根据上面两个方法的参数,进行绘制
*/
public class SwitchButton extends View implements View.OnClickListener{
private Bitmap backgroundBitmap;
private Bitmap slidingBitmap;
private int slidMarginLeftMax;
private Paint mBitMapPaint;
private Paint mDefaultTextPaint;
private Paint mSelectTextPaint;
private int slidingMarginTop;
private int slidingMarginLeft = 5;
private int textVerticalLocation;
private int textLeftMargin = 20;
//是否选择左边
private boolean isSelectLeft = true;
//默认点击事件生效 ,滑动事件不生效
private boolean isEnableClick = true;
//横向滑动的X轴起点坐标
private float startX;
private float isStartX;
private String passwordLoginStr;
private String smsLoginStr;
private ChangeSelectListener mChangeSelectListener;
//判断与之监听的 ViewPager 是否滑动结束 , 只有当滑动结束的时候文字才变化。
/*private boolean isChangeComplete = false ;
private boolean isFirstDraw = true;*/
public SwitchButton(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView(){
backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.login_select_liner_bg);
slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.login_selected_text_bg);
passwordLoginStr = getResources().getString(R.string.password_login);
smsLoginStr = getResources().getString(R.string.sms_login);
slidMarginLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth() - 5;
slidingMarginTop = (backgroundBitmap.getHeight() - slidingBitmap.getHeight()) /2 - 2;
textVerticalLocation = backgroundBitmap.getHeight()/2+5;
mBitMapPaint = new Paint();
mBitMapPaint.setAntiAlias(true);
mDefaultTextPaint = new Paint();
mDefaultTextPaint.setAntiAlias(true);
mDefaultTextPaint.setTextSize(32);
mDefaultTextPaint.setStyle(Paint.Style.FILL);
mDefaultTextPaint.setTextAlign(Paint.Align.LEFT);
mDefaultTextPaint.setColor(Color.WHITE);
mSelectTextPaint = new Paint();
mSelectTextPaint.setAntiAlias(true);
mSelectTextPaint.setTextSize(32);
mSelectTextPaint.setStyle(Paint.Style.FILL);
mSelectTextPaint.setTextAlign(Paint.Align.LEFT);
mSelectTextPaint.setColor(getResources().getColor(R.color.orange));
setOnClickListener(this);
}
/**
* 测量这个view的大小
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
}
/**
* 进行绘制
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(backgroundBitmap,0 ,0, mBitMapPaint);
canvas.drawBitmap(slidingBitmap,slidingMarginLeft,slidingMarginTop, mBitMapPaint);
drawText(canvas);
}
/**
* 绘制文本
* @param canvas
*/
private void drawText(Canvas canvas) {
if (isSelectLeft) {
canvas.drawText(passwordLoginStr, textLeftMargin, textVerticalLocation, mSelectTextPaint);
canvas.drawText(smsLoginStr, slidMarginLeftMax + textLeftMargin, textVerticalLocation, mDefaultTextPaint);
} else {
canvas.drawText(passwordLoginStr, textLeftMargin, textVerticalLocation, mDefaultTextPaint);
canvas.drawText(smsLoginStr, slidMarginLeftMax + textLeftMargin, textVerticalLocation, mSelectTextPaint);
}
}
//实现控件的点击事件
@Override
public void onClick(View v) {
if(isEnableClick) {
isSelectLeft = !isSelectLeft;
if (isSelectLeft) {
slidingMarginLeft = 5;
} else {
slidingMarginLeft = slidMarginLeftMax;
}
if(mChangeSelectListener != null && isEnableClick) {
mChangeSelectListener.changeSelect(isSelectLeft);
}
invalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//计算按下的坐标
isStartX = startX = event.getX();
isEnableClick = true;
break;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float distanceX = endX - startX;
slidingMarginLeft += distanceX;
//屏蔽非法值
if(slidingMarginLeft > slidMarginLeftMax){
slidingMarginLeft = slidMarginLeftMax;
}else if(slidingMarginLeft < 5){
slidingMarginLeft = 5;
}
//isChangeComplete = false;
invalidate();
//数据还原
startX = event.getX();
if(Math.abs(endX - isStartX) > 5){
//已经滑动了 就不再执行点击事件
isEnableClick = false;
}
break;
case MotionEvent.ACTION_UP:
if(!isEnableClick) {
if (slidingMarginLeft > slidMarginLeftMax / 2) {
slidingMarginLeft = slidMarginLeftMax;
isSelectLeft = false;
} else {
slidingMarginLeft = 5;
isSelectLeft = true;
}
invalidate();
}
break;
}
return true;
}
public boolean isSelectLeft() {
return isSelectLeft;
}
public void setSelectLeft(boolean selectLeft) {
isSelectLeft = selectLeft;
if (isSelectLeft) {
slidingMarginLeft = 5;
} else {
slidingMarginLeft = slidMarginLeftMax;
}
invalidate();
}
public void setSlidingMarginLeft(float scaleX){
if(scaleX != 0) {
slidingMarginLeft = (int) (slidMarginLeftMax * scaleX);
invalidate();
}
}
public interface ChangeSelectListener{
void changeSelect(boolean isSelectLeft);
}
public void setChangeSelectListener(ChangeSelectListener changeSelectListener){
mChangeSelectListener = changeSelectListener;
}
}