仿IOS开关自定义View的实现

1.概述

Android中关于控制开关和页面/状态切换的使用场景还是比较多的。源生做的支持也有比如RadioGroup 和Tabhost等。这里准备通过自定义View来模仿学习下IOS两种常见UI样式:SwitchButton 和 SegmentControl 。

首先先通过简易的组装View来实现两种UI的相应效果,其次呢,尝试通过绘制来达到同样的更灵活的样式。代码前后共实现按钮切换和页面切换两个样式,三种实现方案,其中,两种SwitchButton实现,一种SegmentControl实现。实现方案中关于自定义View绘制,本篇只讲述SwitchView,希望大家能举一反三,同样做到SegmentControl的相同效果。个人也更倾向于使用自定义实现,更方便灵活。

先看效果图:

仿IOS开关自定义View的实现_第1张图片

头部即为切换页面的SegmentControl,然后第一行是通过组装view来实现SwitchButton,第二行则是完全绘制出来的SwitchButton效果。接下来我们分别一一讲述代码实现。


2.SwitchButton样式两种实现

状态开关按钮常用于某些控制开关,设置选项里最为常见。

2.1组合View实现

该方法比较简单明了,定义三个view,开启状态和关闭状态两个背景View,一个圆形按钮view。点击时候利用滑动动画移动按钮和状态背景,达到类似的视觉效果。先看xml布局:

[XML】


  
    
    
  
  


因为是帧布局,所以顶层使用merge(merge简化xml不解释,自行百度)。然后使用两个开关状态背景和一个圆形按钮组合而成。

1.全局变量参数

public class SwitchView extends FrameLayout {
  protected boolean isChecked;  //是否选中状态
  protected View onBgView;
  protected View offBgView;
  protected View circleView;
  protected boolean autoForPerformClick = true; //是否允许点击自动切换
  protected OnCheckedChangedListener onCheckedChangedListener; //切换事件监听
  //...
}


一般状态切换是由click事件监听,根据业务逻辑来判断是否切换状态。但对于switchButton,通常我们操作时直观感受应该是先切换了状态才执行相应操作的,所以我们在performClick事件中直接根据autoForPerformClick 的状态来相应点击操作。

至于performClick ,其实就是控制条用onClickListener的方法体,具体逻辑在View源码中查看。


2.初始化

  
public SwitchView(Context context) {  super(context);
  initialize();
}
public SwitchView(Context context, AttributeSet attrs) {
  super(context, attrs);
  initialize();
}
public SwitchView(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  initialize();
}
protected void initialize() {
  setClickable(true);
  LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  layoutInflater.inflate(R.layout.switch_view, this);
  onBgView = findViewById(R.id.on_bg_view);
  offBgView = findViewById(R.id.off_bg_view);
  circleView = findViewById(R.id.circle_view);
}

3.点击响应

@Overridepublic boolean performClick() { 
  if (!autoForPerformClick) //如果不是自动响应则调用默认处理方法
    return super.performClick();
  /**
  *否则直接切换switch状态并触发事件监听
  */
  setChecked(!isChecked, true);
  if (onCheckedChangedListener != null) {
    onCheckedChangedListener.onChanged(this, isChecked);
  }
  return super.performClick();
}


View点击后会执行performClick方法,并判断是否调用clickLisentener。这里我们直接重写performClick方法,如果自动响应autoForPerformClick为ture则直接切换Switch状态,否则调用默认处理逻辑。

4.切换状态动画

点击打开,则圆形按钮从左端滑动到右端,onBg显示,offBg隐藏;再点击关闭,圆形按钮从右端滑动到左端,onBg隐藏,offBg显示。

public void setChecked(boolean value, boolean needAnimate) {
    if (isChecked == value)
  return;
    isChecked = value;
    float targetX = 0; //要移动的目标位置
    if (getWidth() != 0) {  //当前view没有渲染上去时候,getWidth()为零
  targetX = getWidth() - circleView.getWidth();
    } else {
  measure(0, 0);
  targetX = getMeasuredWidth() - circleView.getMeasuredWidth();
    }
    long durationMillis = needAnimate ? 200 : 0;
    if (isChecked) {
  onBgView.bringToFront(); //显示在最前端
  onBgView.setVisibility(View.VISIBLE);
  offBgView.setVisibility(View.VISIBLE);
  //平移动画
  TranslateAnimation an1 = new TranslateAnimation(0, targetX, 0, 0);
  an1.setFillAfter(true);
  an1.setDuration(durationMillis);
  circleView.startAnimation(an1);
  //透明度动画
  AlphaAnimation an2 = new AlphaAnimation(0, 1);
  an2.setFillAfter(true);
  an2.setDuration(durationMillis);
  onBgView.startAnimation(an2);
    } else {
  offBgView.bringToFront();
  onBgView.setVisibility(View.VISIBLE);
  offBgView.setVisibility(View.VISIBLE);
  TranslateAnimation an1 = new TranslateAnimation(targetX, 0, 0, 0);
  an1.setFillAfter(true);
  an1.setDuration(durationMillis);
  circleView.startAnimation(an1);
  AlphaAnimation an2 = new AlphaAnimation(0, 1);
  an2.setFillAfter(true);
  an2.setDuration(durationMillis);
  offBgView.startAnimation(an2);
    }
}


状态切换的两个参数,value是否打开状态,needAnimate是否需要动画(否则直接切换效果)。setFillAfter保留动画结束状态,但并不影响View本身位置和状态。切换时,先将当前显示背景移动到最前端,其次添加按钮动画和渐隐动画。

至此,最基本的组合View实现已经完成了。想要了解详情的请在源码中查看。源码分为两部分,一个项目是View的实现lib,另一块是示例演示demo.


2.2自定义View绘制实现

由于该样式并不十分复杂,所以可以通过基本的图形绘制draw出同样的效果。

具体实现逻辑:通过自定view属性来确定按钮大小和中间圆钮大小,在测量onMesure方法中控制测量值mode和Size,并在onLayout方法中得到圆钮半径和起始点位置。然后进行绘制,先绘制底部on圆角矩形背景,再绘制off渐变缩放的圆角矩形,最后绘制spot圆钮。

嘴比较笨拙,又不会画图。用word的图形工具将就画下可以看就好了。

仿IOS开关自定义View的实现_第2张图片

具体实现大体都类似,这里贴上主要部分代码


1.全局参数

public class SwitchButton extends View{
  /** */
  private float radius;
  /** 开启颜色*/
  private int onColor = Color.parseColor("#4ebb7f");
  /** 关闭颜色*/
  private int offBorderColor = Color.parseColor("#dadbda");
  /** 灰色带颜色*/
  private int offColor = Color.parseColor("#ffffff");
  /** 手柄颜色*/
  private int spotColor = Color.parseColor("#ffffff");
  /** 边框颜色*/
  private int borderColor = offBorderColor;
  /** 画笔*/
  private Paint paint ;
  /** 开关状态*/
  private boolean toggleOn = false;
  /** 边框大小*/
  private int borderWidth = 2;
  /** 垂直中心*/
  private float centerY;
  /** 按钮的开始和结束位置*/
  private float startX, endX;
  /** 手柄X位置的最小和最大值*/
  private float spotMinX, spotMaxX;
  /**手柄大小 */
  private int spotSize ;
  /** 手柄X位置*/
  private float spotX;
  /** 关闭时内部灰色带高度*/
  private float offLineWidth;
  /** */
  private RectF rect = new RectF();
  /** 默认使用动画*/
  private boolean defaultAnimate = true;
  private OnSwitchChanged listener;
  //...
}


2.初始化与读取

读取自定义属性并赋值。讲了又讲的东西,略。

3.测量onMeasure与布局onLayout

在onMeasure方法中根据给定mode和size来限定View,如果高宽不为明确值(UNSPECIFIED/AT_MOST),则定义自身高宽为明确值。 关于MeasureSpec的详细讲解,这里附上 爱哥的一篇文章–MeasureSpec (http://blog.csdn.net/aigestudio/article/details/38636531),深入到赋值读取的内部,不妨试着深入研究下。当然,更直接的方法就是点开源码一探究竟咯。

onLayout方法中取得view的实际高宽,计算出圆角矩形半径,圆钮半径以及起始点x方向位置。还有On矩形和off矩形的宽度。


  
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  int widthSize = MeasureSpec.getSize(widthMeasureSpec);
  int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  /**
  *如果高宽未指定,则使用内置高宽明确大小
  */
  Resources r = Resources.getSystem();
  if(widthMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.AT_MOST){
    widthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, r.getDisplayMetrics());
    widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
  }
  if(heightMode == MeasureSpec.UNSPECIFIED || heightSize == MeasureSpec.AT_MOST){
    heightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, r.getDisplayMetrics());
    heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
  }
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
    int bottom) {
  super.onLayout(changed, left, top, right, bottom);
  final int width = getWidth();
  final int height = getHeight();
  /**
  *测量相应大小
  */
  radius = Math.min(width, height) * 0.5f;
  centerY = radius;
  startX = radius;
  endX = width - radius;
  spotMinX = startX + borderWidth;
  spotMaxX = endX - borderWidth;
  spotSize = height - 4 * borderWidth;
  spotX = toggleOn ? spotMaxX : spotMinX;
  offLineWidth = 0;
}

前三步完成基本赋值之后,开始设置和绑定相应事件。这里不作为重点部分也省略,主要讲一下绘制过程和核心控制逻辑。

4.绘制过程

按照前面的简易示例图来绘制我们的ui图。

@Override
public void draw(Canvas canvas) {
  //绘制on背景
  rect.set(0, 0, getWidth(), getHeight());
  paint.setColor(borderColor);
  canvas.drawRoundRect(rect, radius, radius, paint);
  //绘制off背景(缩放至0时候不绘制)
  if(offLineWidth > 0){
    final float cy = offLineWidth * 0.5f;
    rect.set(spotX - cy, centerY - cy, endX + cy, centerY + cy);
    paint.setColor(offColor);
    canvas.drawRoundRect(rect, cy, cy, paint);
  }
  //绘制圆钮轮廓border
  rect.set(spotX - 1 - radius, centerY - radius, spotX + 1.1f + radius, centerY + radius);
  paint.setColor(borderColor);
  canvas.drawRoundRect(rect, radius, radius, paint);
  //绘制圆钮
  final float spotR = spotSize * 0.5f;
  rect.set(spotX - spotR, centerY - spotR, spotX + spotR, centerY + spotR);
  paint.setColor(spotColor);
  canvas.drawRoundRect(rect, spotR, spotR, paint);
}


及诶按来便是我们的状态切换动画控制逻辑,即点击按钮之后setToggleOn或者setToggleOff执行的相应动作。

4.状态切换动画效果

/*** 执行效果,如果animate为true表示有动画效果
* 否则直接执行计算并显示最终打开"1"或者关闭"0"的效果绘制
*/
private void takeEffect(boolean animate) {
  if(animate){
    slide();
  }else{
    calculateEffect(toggleOn ? 1 : 0);
  }
}
/**
*这里偷个懒,直接使用空的animation,根据当前interpolatedTime(0~1)渐变过程来绘制不同阶段的View,达到动画效果
*当然,也可以开启个线程或者定时任务,来实现从0到1的变换,劲儿改变视图绘制过程
*/
private void slide(){
    Animation animation = new Animation() {
      @Override
      protected void applyTransformation(float interpolatedTime,
          Transformation t) {
        if(toggleOn){
          calculateEffect(interpolatedTime);
        }else{
          calculateEffect(1-interpolatedTime);
        }
      }
    };
    animation.setDuration(200);
    clearAnimation();
    startAnimation(animation);
}
/**
*计算绘制位置
*mapValueFromRangeToRange方法计算从当前位置相对于目标位置所对应的值
*通过颜色变化来达到透明度动画效果(颜色渐变)
*/
private void calculateEffect(final double value) {
  final float mapToggleX = (float) mapValueFromRangeToRange(value, 0, 1, spotMinX, spotMaxX);
  spotX = mapToggleX;
  float mapOffLineWidth = (float) mapValueFromRangeToRange(1 - value, 0, 1, 10, spotSize);
  offLineWidth = mapOffLineWidth;
  final int fb = Color.blue(onColor);
  final int fr = Color.red(onColor);
  final int fg = Color.green(onColor);
  final int tb = Color.blue(offBorderColor);
  final int tr = Color.red(offBorderColor);
  final int tg = Color.green(offBorderColor);
  int sb = (int) mapValueFromRangeToRange(1 - value, 0, 1, fb, tb);
  int sr = (int) mapValueFromRangeToRange(1 - value, 0, 1, fr, tr);
  int sg = (int) mapValueFromRangeToRange(1 - value, 0, 1, fg, tg);
  sb = clamp(sb, 0, 255);
  sr = clamp(sr, 0, 255);
  sg = clamp(sg, 0, 255);
  borderColor = Color.rgb(sr, sg, sb);
  postInvalidate();
}


以上就是自定义View绘制的核心代码,详细查看 源码SwitchButton(http://download.csdn.net/detail/u011254308/8860005)。相较于组合方法,它更便捷,也有更高的灵活性和扩展性。同时还不需要图片资源支持。

3.SegmentControl样式实现

常见的Tab有很多种,这里使用的是IOS常见的一种切换效果SegmentControl。本篇只用最简单的拼装View实现类似效果。有兴趣的可以自己尝试绘制达到更优效果。(有空的话也会在后边放出)

  • 通过view组合生成 最近单的方案,没有之一。使用现成的selector和背景来控制显示效果。各个子view分别继承 RelativeLayout并实现OnClick接口。最后在Segment中控制显示和点击切换。
  • 自定义View绘制生成 这里只是提供思路。定义一个ItemView,根据在Segment中位置挥之不同效果。背景效果会用selector.xml的都知道,使用shape标签产生的drawable对象,其实就是一个 GradientDrawable 。所以我们自定义view可以直接通过使用GradientDrawable的 setCornerRadii(float[] radii) 来绘制同样的背景效果,劲儿可以做到不同颜色。最后,使用一个ViewGroup不含这些item即可。通过click事件来切换tab就可以了。
3.1组合View实现

首先,类似的定义一个 可点击的通用的RelativLayout 。(实现 Checkable接口使其可被选中也移除选中状态,详细可以参考前面的博文微博/动态 点赞效果)。这里涉及三个新内容,稍微说明讲解下。

checkMode 选中模式,是单选 CHECKMODE_CHECK 还是 CHECKMODE_RADIO 单选效果。使我们的自定义RelativeLayout可以做到单选和复选。

onInitializeAccessibilityEvent 添加View接受事件源信息。即订阅checked事件。由于事件可能由内部子view点击触发,所以这里应该接收并处理相应的checked事件。当然,使用该方法首先要重写onInitializeAccessibilityNodeInfo方法,添加我们关注的状态信息。

SavedState状态保存 当我们内部可能嵌套复杂view的时候,为了防止数据状态丢失,一般需要定义状态保存类,用以保存和恢复当前View状态。


#### 1.可点击的通用RelativeLayout

  • 继承实现Clickable接口 ,简要略过。
  • //定义checked状态
    public static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked };
    //重写SetChecked方法和isChecked方法略
    /**
    *根据当前选择模式checkMode 来控制单复选
    */
    @Override
    public boolean performClick() {
      if (checkMode == CHECKMODE_CHECK) {
        toggle();
      } else if (checkMode == CHECKMODE_RADIO) {
        setChecked(true);
      }
      return super.performClick();
    }
    /**
    *添加Drawable 的checked状态 ,并再绘制view是绘制相应状态效果
    */
    @Override
    public int[] onCreateDrawableState(int extraSpace) {
      int[] states = super.onCreateDrawableState(extraSpace + 1);
      if (isChecked()) {
        mergeDrawableStates(states, CHECKED_STATE_SET);
      }
      return states;
    }
    @Override
    protected void drawableStateChanged() {
      super.drawableStateChanged();
      Drawable drawable = getBackground();
      if (drawable != null) {
        int[] myDrawableState = getDrawableState();
        drawable.setState(myDrawableState);
        invalidate();
      }
    }


  • 接受checked状态事件信息
  • @Overridepublic void onInitializeAccessibilityEvent(AccessibilityEvent event) {
      super.onInitializeAccessibilityEvent(event);
      event.setClassName(CheckedRelativeLayout.class.getName());
      event.setChecked(checked);
    }
    @Override
    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
      super.onInitializeAccessibilityNodeInfo(info);
      info.setClassName(CheckedRelativeLayout.class.getName());
      info.setCheckable(true);
      info.setChecked(checked);
    }


  • 保存View状态和恢复 View自身重写保存和恢复的方法
  • @Overridepublic Parcelable onSaveInstanceState() {//保存
      Parcelable superState = super.onSaveInstanceState();
      SavedState ss = new SavedState(superState);
      ss.checked = isChecked();
      return ss;
    }
    @Override
    public void onRestoreInstanceState(Parcelable state) {//恢复
      SavedState ss = (SavedState) state;
      super.onRestoreInstanceState(ss.getSuperState());
      setChecked(ss.checked);
      requestLayout();
    }
    


用于保存数据的基本状态类型

static class SavedState extends BaseSavedState {
  boolean checked;
  SavedState(Parcelable superState) {
    super(superState);
  }
  private SavedState(Parcel in) {
    super(in);
    checked = (Boolean) in.readValue(null);
  }
  @Override
  public void writeToParcel(Parcel out, int flags) {
    super.writeToParcel(out, flags);
    out.writeValue(checked);
  }
  public static final Creator CREATOR = new Creator() {
    public SavedState createFromParcel(Parcel in) {
      return new SavedState(in);
    }
    public SavedState[] newArray(int size) {
      return new SavedState[size];
    }
  };
}
  @Override
  public String toString() {
    return "CompoundButton.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " checked=" + checked + "}";
  }


2.控制tab切换的SegmentView

代码比较易于理解,这里直接贴出来查阅即可。基本思路,水平线性布局包裹对应左中右不同item个数的选项,并通过设置对应left/right/center来设置背景。然后分别为每个Item设置同一个点击事件,点击之后检查是否当前item被选中,改变statu,同时出发切换事件。详细代码:

public class SegmentView extends LinearLayout {
  protected final static int SEGMENT_LEFT_BG = R.drawable.segment_left_selector;
  protected final static int SEGMENT_CENTER_BG = R.drawable.segment_center_selector;
  protected final static int SEGMENT_RIGHT_BG = R.drawable.segment_right_selector;
  protected int leftBg = SEGMENT_LEFT_BG;
  protected int centerBg = SEGMENT_CENTER_BG;
  protected int rightBg = SEGMENT_RIGHT_BG;
  protected CheckedRelativeLayout2[] checkedRelativeLayouts;
  protected int index = -1;
  protected float textSize = -1;
  protected int textColorN = Color.BLACK, textColorP = Color.BLACK;
  protected OnIndexChangedListener onIndexChangedListener;
  public SegmentView(Context context) {
    super(context);
    initialize();
  }
  public SegmentView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initialize();
    initFromAttributes(context, attrs);
  }
  public SegmentView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initialize();
    initFromAttributes(context, attrs);
  }
  protected void initialize() {
    setGravity(Gravity.CENTER);
  }
  protected void initFromAttributes(Context context, AttributeSet attrs) {
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SegmentView);
    String content = a.getString(R.styleable.SegmentView_content);
    index = a.getInt(R.styleable.SegmentView_index, index);
    textSize = a.getDimension(R.styleable.SegmentView_textSize, textSize);
    textColorN = a.getColor(R.styleable.SegmentView_textColorN, textColorN);
    textColorP = a.getColor(R.styleable.SegmentView_textColorP, textColorP);
    leftBg = a.getResourceId(R.styleable.SegmentView_leftBg, leftBg);
    centerBg = a.getResourceId(R.styleable.SegmentView_centerBg, centerBg);
    rightBg = a.getResourceId(R.styleable.SegmentView_rightBg, rightBg);
    a.recycle();
    if (!TextUtils.isEmpty(content)) {
      String[] contentStrings = content.split(",");
      setContent(contentStrings);
    }
    setIndex(index);
  }
  public void setContent(String... content) {
    View[] views = new View[content.length];
    for (int i = 0, len = content.length; i < len; i++) {
      String s = content[i];
      TextView tv = new TextView(getContext());
      tv.setTextColor(textColorN);
      tv.setText(s);
      if (textSize != -1) {
        tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
      }
      views[i] = tv;
    }
    setContent(views);
  }
  public void setContent(View... content) {
    removeAllViews();
    int lastIndex = content.length - 1;
    checkedRelativeLayouts = new CheckedRelativeLayout2[content.length];
    checkedRelativeLayouts[0] = createLeftView(content[0]);
    checkedRelativeLayouts[lastIndex] = createRightView(content[lastIndex]);
    for (int i = 1; i < lastIndex; i++) {
      checkedRelativeLayouts[i] = createCenterView(content[i]);
    }
    for (View view : checkedRelativeLayouts) {
      LayoutParams llp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
      llp.weight = 1;
      addView(view, llp);
    }
  }
  public int getIndex() {
    return index;
  }
  public void setIndex(int i) {
    if (i < 0)
      return;
    checkedRelativeLayouts[i].setChecked(true);
  }
  public void setTextColorN(int textColorN) {
    this.textColorN = textColorN;
  }
  public void setTextColorP(int textColorP) {
    this.textColorP = textColorP;
  }
  protected CheckedRelativeLayout.OnCheckedChangeListener checkedChangeListener = new CheckedRelativeLayout.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CheckedRelativeLayout layout, boolean isChecked) {
      if (isChecked) {
        for (CheckedRelativeLayout2 item : checkedRelativeLayouts) {
          if (!item.equals(layout)) {
            item.setChecked(false);
          }
        }
        if (onIndexChangedListener != null) {
          int i = indexOf(checkedRelativeLayouts, layout);
          index = i;
          if (onIndexChangedListener != null) {
            onIndexChangedListener.onChanged(SegmentView.this, index);
          }
        }
      }
    }
  };
  protected CheckedRelativeLayout2 createLeftView(View contentView) {
    CheckedRelativeLayout2 layout = new CheckedRelativeLayout2(getContext());
    layout.setBackgroundResource(leftBg);
    layout.setGravity(Gravity.CENTER);
    layout.addView(contentView);
    layout.setOnCheckedChangeListener(checkedChangeListener);
    return layout;
  }
  protected CheckedRelativeLayout2 createCenterView(View contentView) {
    CheckedRelativeLayout2 layout = new CheckedRelativeLayout2(getContext());
    layout.setBackgroundResource(centerBg);
    layout.setGravity(Gravity.CENTER);
    layout.addView(contentView);
    layout.setOnCheckedChangeListener(checkedChangeListener);
    return layout;
  }
  protected CheckedRelativeLayout2 createRightView(View contentView) {
    CheckedRelativeLayout2 layout = new CheckedRelativeLayout2(getContext());
    layout.setBackgroundResource(rightBg);
    layout.setGravity(Gravity.CENTER);
    layout.addView(contentView);
    layout.setOnCheckedChangeListener(checkedChangeListener);
    return layout;
  }
  public void setOnIndexChangedListener(OnIndexChangedListener l) {
    this.onIndexChangedListener = l;
  }
  protected class CheckedRelativeLayout2 extends CheckedRelativeLayout {
    protected TextView textView;
    public CheckedRelativeLayout2(Context context) {
      super(context);
    }
    @Override
    public void addView(View child) {
      super.addView(child);
      if (child instanceof TextView) {
        textView = (TextView) child;
      }
    }
    @Override
    public void setChecked(boolean checked) {
      super.setChecked(checked);
      if (textView != null) {
        if (checked) {
          textView.setTextColor(textColorP);
        } else {
          textView.setTextColor(textColorN);
        }
      }
    }
  }
  public static interface OnIndexChangedListener {
    public void onChanged(SegmentView view, int index);
  }
  public static  int indexOf(T[] array, T obj) {
    for (int i = 0, len = array.length; i < len; i++) {
      if (array[i].equals(obj))
        return i;
    }
    return -1;
  }
}


该方法比较简陋,背景颜色定制性不高。即只能通过既定drawable北京来实现。不过,其实是可以通过selector来定义相关背景drawable的。不妨试一下。

3.2自定义View实现

本来此方法只是简单提及的一个想法而已,今天有空就一并写了。时间匆忙,代码稍微有些混乱,不过还是能起到一定示范效用的,这里也贴出来供大家参考。

整体思路:


  • 定义子item设置其选中状态和字体/背景色。通过测量方法保证显示范围和字体大小,通过GradientDrawable绘制圆角背景,并画对应字体。
    • 定义Segment继承自ViewGroup,读取自定义属性,根据文本内容添加子View。然后重写OnMeasure方法和OnLayout方法来测量和布局子View。最后添加点击事件,提供监听接口。
      代码如下:

import com.qiao.demo.R;
import com.qiao.demo.R.styleable;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
public class SegmentView extends ViewGroup implements OnClickListener{
  private final float r = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics());
  private int bgColor = 0xff0072c6;
  private int fgColor = Color.WHITE;
  private float mTextSize = 3f*r;
  private String []mText= {"item1","item2","item3"};
  private int checkedItem=1;
  private OnItemClickListener listener;
  public SegmentView(Context context) {
    super(context);
    initFromAttributes(context, null);
    initalize();
  }
  public SegmentView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initFromAttributes(context,attrs);
    initalize();
  }
  protected void initFromAttributes(Context context, AttributeSet attrs) {
    if(attrs==null) return;
    TypedArray a = context.obtainStyledAttributes(attrs,  R.styleable.SegmentView0);
    String content = a.getString(R.styleable.SegmentView0_content0);
    if(!isEmpty(content)){
      mText = content.split(",");
    }
    checkedItem = a.getInt(R.styleable.SegmentView0_index0, checkedItem);
    mTextSize = a.getDimension(R.styleable.SegmentView0_textSize0, mTextSize);
    bgColor = a.getColor(R.styleable.SegmentView0_bgColor, bgColor);
    fgColor = a.getColor(R.styleable.SegmentView0_textColor, fgColor);
    a.recycle();
  }
  public void initalize(){
    int length = mText.length;
    for(int i=0;i=0){
      maxWidth = widthSize/count;
      childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth,widthMode);
    }
    for(int i=0;i=0){
        float textSize = Math.min(mTextSize,height-2*r);
        if(width>0){
          textSize = Math.min(textSize,(width-2*r)*2/text.length()); //英文比中文短(中文为两个字符),故取mText.length()/2作为平均宽度
        }
        if(textSize != mTextSize ){
          mTextPaint.setTextSize(textSize);
          mTextPaint.getTextBounds(text, 0, text.length(), mTextBound);
        }
      }
    }
    @Override
    public void draw(Canvas canvas) {
      Rect rect = canvas.getClipBounds();
      drawable.setBounds(new Rect(rect));
      drawable.draw(canvas);
      int l = (rect.width() - mTextBound.width())/2;
      int b = (rect.height() + mTextBound.height())/2;
      canvas.drawText(text, l, b, mTextPaint);
    }
  }
  public void setOnItemClickListener(OnItemClickListener onItemClickListener){
    this.listener = onItemClickListener;
  }
  interface OnItemClickListener{
    void onItemClick(ItemView item,int checkedItem);
  }
  public static boolean isEmpty(String str){
    return null==str || str.trim().length() == 0;
  }
}


参照前面两段讲述完全可以理解了。使用时候可以方便的通过自定义属性来控制字体颜色和点击背景。可以动态变更View高宽。有问题的同学可以在文末提出或指正。

3.总结

感觉自己学习进步的速度很慢,常常伴随着焦急浮躁。这篇文章也是积累了好久才慢吞吞的写完了。代码方面,个人也有不少不良习惯,助事业不够清晰,不过总体上不是有碍观瞻吧。

同样的东西,尝试用不同想法写两遍,我觉得是有好处的。至少于我,能看到不少有意思的东西。

最后, 附上本文的示例源码 . 由于资源上传较早,第二部分的自定义View并没有打包上传。不过上便已经贴出完整代码了,可以直接拿来使用。



你可能感兴趣的:(Android)