望而生畏之自定义View

社会就是这样,你又不能改变社会,只能适应。

  本篇文章作者会不断更新,小伙伴可以持续鼓励支持。不支持就停更,就是这么傲娇。github同步代码https://github.com/liulingfeng/CustomView

对于很多Android开发者而言,自定义view简直就是噩梦。想想都觉得好难,但自定义view又是高级进阶的必经之路。下面就我所知道的来讲一下自定义view吧,废话不多说,打卡上车。我将自定义view分为以下三类:

  • 组合控件--代码复用
  • 继承已有控件或ViewGroup--功能扩展
  • 纯自定义,继承View

组合控件

并不需要我们自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但我们可以将几个系统原生的控件组合到一起,这样创建出的控件就被称为组合控件。我们不生成控件,我们只是控件的搬运工。下面我就来创建一个公共的头部。
<pre>```<RelativeLayout 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:background="@color/colorAccent"
android:orientation="vertical" >
<TextView
android:id="@+id/tv_head"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="This is Title"
android:textSize="18sp"
android:textColor="@android:color/white"
android:gravity="center"
android:layout_centerInParent="true"
/>
<ImageView
android:id="@+id/iv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:src="@mipmap/back"
android:layout_marginLeft="8dp"
/>
</RelativeLayout>

     头部布局
</pre>

###### 在这里分享一个工具tools,在上面的布局中你可以发现它的身影。我给Textview设置的是  tools:text,而不是  android:text。这样设置的好处是这个文字只会在预览中看到,在正式运行到手机上是不会看到的。这个工具在ListView中特别好用,可以在ListView中设置item预览样式。
<pre>
```public class HeadView extends RelativeLayout implements View.OnClickListener{    
      private TextView tv_head;    
      private ImageView iv_back;    
      private Context mContext;   
      public HeadView(Context context) {  this(context,null); }   
      public HeadView(Context context, AttributeSet attrs) {this(context, attrs,0);}    
      public HeadView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr);       
            mContext = context;        
          //第一个参数:布局的资源id 第二个参数:填充的根视图 第三个参数是否将载入的视图绑定到根视图中             
           LayoutInflater.from(context).inflate(R.layout.layout_head,this,true);        
           tv_head = (TextView)findViewById(R.id.tv_head);        
           iv_back = (ImageView) findViewById(R.id.iv_back);        
           iv_back.setOnClickListener(this);   
 }    
    public void setTitle(String headTv){       
        tv_head.setText(headTv);   
  }   
 @Override    
    public void onClick(View view) {       
        if(view == iv_back){          
        ((Activity)mContext).finish();       
      }   
  }
}```
     自定义的头部
</pre>
*我这边简单的来说一下这里面三个构造方法的使用场景。第一种是在java代码中直接创建时使用;第二种是在xml定义时使用,但不引用style设置样式;第三种用脚趾头想都知道是在引用style设置样式时使用。我这边一股脑儿的把所有构造方法都放在第三种中处理,这是一种较为简便的写法。*
</p>
这个自定义组合控件的用法我就不详细说了,只需要像其他布局一样<类的完整地址></类的完整地址>

![大概结果.png](http://upload-images.jianshu.io/upload_images/1976633-7bf499842defb976.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

###### 好了姿势对了,接下来我再来奉献一个常用的组合控件--可删除的搜索框
<pre>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/searchbar_width"
android:layout_height="@dimen/searchbar_height"
android:layout_marginTop="16dp"
android:background="@drawable/searchbar_bg"

<ImageView
android:id="@+id/delete"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@mipmap/close"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="10dp"
android:visibility="invisible"
/>
<View
android:id="@+id/divider"
android:layout_width="1dp"
android:layout_height="24dp"
android:background="@color/colorAccent"
android:layout_toLeftOf="@id/delete"
android:layout_marginRight="6dp"
android:layout_centerVertical="true"
/>
<EditText
android:id="@+id/input"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toLeftOf="@id/divider"
android:paddingLeft="12dp"
android:background="@null"
android:lines="1"
/>
</RelativeLayout>

     搜索框布局
</pre>

###### 我这里的外边框用的是shape,而不是图片。至于为什么用shape,各位看官可以看我的另一票文章<http://www.jianshu.com/p/73317378d18c>。不知道如何写的同学可以咨询我。

<pre>
public class SearchBar extends LinearLayout implements View.OnClickListener,TextWatcher{    
   private ImageView delete;    
   private EditText input;    
   public SearchBar(Context context) {this(context,null);}    
   public SearchBar(Context context, AttributeSet attrs) {this(context, attrs,0);}    
   public SearchBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);       
        View.inflate(context, R.layout.layout_searchbar,this);        
        delete = (ImageView)findViewById(R.id.delete);       
        input = (EditText)findViewById(R.id.input);        
        delete.setOnClickListener(this);        
        input.addTextChangedListener(this);   
 }    
   @Override    
   public void onClick(View view) {       
      switch (view.getId()){            
            case R.id.delete:               
            input.setText("");               
            delete.setVisibility(View.INVISIBLE);                
            break;        
         }   
 }    
   @Override    
   public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {    }    
   @Override   
   public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {   }    
   @Override    
   public void afterTextChanged(Editable editable) {           
     if(editable.length()!=0){           
           delete.setVisibility(View.VISIBLE);        
       }
   else{            
         delete.setVisibility(View.INVISIBLE);       
       }    
   }
}
     自定义的搜索框
</pre>

###### 之前没有写明使用的正确姿势,昨天晚上我一直辗转反侧睡不着。在这里我想应该写一下使用的正确姿势。

<pre>
<com.llf.loding.view.SearchBar xmlns:android="http://schemas.android.com/apk/res/android"              
      android:layout_width="match_parent"              
      android:layout_height="match_parent"              
      android:orientation="vertical"              
      android:gravity="center_horizontal"    >    
  <View        
      android:layout_width="match_parent"        
      android:layout_height="match_parent"        
      android:background="@color/colorPrimary"        
      android:layout_marginTop="16dp"        
></View>
</com.llf.loding.view.SearchBar>
使用的正确姿势,我这样使用可以减少一层布局
</pre>

###### 最后上结果图
![input.gif](http://upload-images.jianshu.io/upload_images/1976633-9c669445d7a52ddc.gif?imageMogr2/auto-orient/strip)
> 不要带有目的性的去做一件事情,这样往往达不到目的。

###### 好的,咱们换个高深点的姿势,来自定义一个头部广告。这边有两种思路,一种是用ViewPager;一种是用ViewFlipper。我这边是用ViewFlipper实现。

<pre>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"                
             android:layout_width="match_parent"                
             android:layout_height="@dimen/iv_height"                
             android:orientation="vertical">    
   <com.llf.loding.view.NotifiableViewFlipper        
         android:id="@+id/viewflipper"       
         android:layout_width="match_parent"        
         android:layout_height="wrap_content"        
         android:autoStart="true"        
         android:inAnimation="@anim/slide_in"        
         android:outAnimation="@anim/slide_out"       
         android:flipInterval="2000"        
         android:persistentDrawingCache="animation"        
      />   
   <LinearLayout        
         android:id="@+id/indicator"       
         android:layout_width="match_parent"        
         android:layout_height="40dp"        
         android:layout_alignParentBottom="true"        
         android:gravity="center"        
         android:orientation="horizontal"        
         android:background="#15000000"       
     />
</RelativeLayout>
     广告位布局
</pre>

######  细心的同学可能已经发现我在这里不小心用了另外一招--继承已有控件。大家不要打我,我这里也是逼不得已,这个ViewFlipper很奇葩,没有滑动到哪个view的回调接口,大家看下去就会明白我不是故意的。这里先上这个自定义的ViewFlipper。

<pre>
public class NotifiableViewFlipper extends ViewFlipper {    
     private OnFlipListener onFlipListener;    
     public static interface OnFlipListener {        
          public void onShowPrevious(NotifiableViewFlipper flipper);        
          public void onShowNext(NotifiableViewFlipper flipper);    
     }    
     public void setOnFlipListener(            
          OnFlipListener onFlipListener) {        
          this.onFlipListener = onFlipListener;    
     }    
     public NotifiableViewFlipper(Context context) {  super(context); }    
     public NotifiableViewFlipper(Context context, AttributeSet attrs) {super(context, attrs);    }    
     @Override    
     public void showPrevious() {        
          super.showPrevious();        
          if (hasFlipListener()) {            
          onFlipListener.onShowPrevious(this);        
            }    
     }    
     @Override    
    public void showNext() {       
         super.showNext();        
         if (hasFlipListener()) {            
              onFlipListener.onShowNext(this);        
          }   
   }    
}
</pre>

###### 该是上主角的时候了,下面是自定义的广告位。
<pre>
public class AdvertisementLayout extends LinearLayout implements NotifiableViewFlipper.OnFlipListener{    
        private Context mContext;   
        private NotifiableViewFlipper viewFlipper;   
        private LinearLayout indicator;    
        private List<Integer> datas;    
        private ImageView[] images;    
        private int currentItem;    
     public AdvertisementLayout(Context context) { this(context,null);}    
     public AdvertisementLayout(Context context, AttributeSet attrs) {  this(context, attrs,0);    }    
     public AdvertisementLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr);        
         mContext = context;       
         init();   
   }    
      private void init(){        
          View.inflate(mContext, R.layout.layout_advertisement,this);        
          viewFlipper = (NotifiableViewFlipper)findViewById(R.id.viewflipper);        
          indicator = (LinearLayout)findViewById(R.id.indicator);        
          viewFlipper.setOnFlipListener(this);    
  }    
      public void setData(List<Integer> data){        
          this.datas = data;       
          instantiation();    
}  
      private void instantiation(){       
          images = new ImageView[datas.size()];       
          for (int i=0;i<datas.size();i++){            
            ImageView imageView = new ImageView(mContext);            
            imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);            
            imageView.setLayoutParams(new 
            ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,(int)mContext.getResources().getDimension(R.dimen.iv_height)));            
            imageView.setImageResource(datas.get(i));            
            imageView.setOnClickListener(new OnClickListener() {                
            @Override                
            public void onClick(View view) {                    
            Toast.makeText(mContext,viewFlipper.getDisplayedChild()+"被点击了",Toast.LENGTH_SHORT).show();                
           }           
    });           
             viewFlipper.addView(imageView);        
   } 
          for (int i=0;i<datas.size();i++){            
              ImageView imageView = new ImageView(mContext);            
              LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(30,30);            
              layoutParams.setMargins(10,2,10,2);            
              imageView.setLayoutParams(layoutParams);            
              imageView.setBackgroundResource(R.drawable.indicator_iv);            
              imageView.setEnabled(false);            
              indicator.addView(imageView);           
              images[i] = imageView;        
            }        
            images[0].setEnabled(true);   
 }   
          @Override    
           public void onShowPrevious(NotifiableViewFlipper flipper) {        
                images[currentItem].setEnabled(false);        
                images[flipper.getDisplayedChild()].setEnabled(true);        
                currentItem = flipper.getDisplayedChild();    
           }    
          @Override    
          public void onShowNext(NotifiableViewFlipper flipper) {        
             images[currentItem].setEnabled(false);        
             images[flipper.getDisplayedChild()].setEnabled(true);       
             currentItem = flipper.getDisplayedChild();    
}

</pre>
###### 上图咯

![advertise.gif](http://upload-images.jianshu.io/upload_images/1976633-1d4c9355ee0ee396.gif?imageMogr2/auto-orient/strip)

> 我们愤怒,不是因为不公平,而是自己处在不公平的位置上。

###### 接下来我们开始讲继承已有View或ViewGroup,说白了就是扩展功能。在讲这个之前我们先来看两个知识点:
###### 1. 涉及到的常用方法
- onFinishInflate()-当应用从XML加载该组件并用它构建界面之后调用的方法。
- onMeasure()-检测View组件及其子组件的大小,此方法执行之后才能获取到控件的大小。
- onLayout()-当该组件需要分配其子组件的位置、大小时。一般在viewGroup中才会使用到此方法。
- onSizeChange()-当该组件的大小被改变时调用。
- onDraw() -当组件将要绘制它的内容时调用。
- onTouchEvent()-当发生触屏事件时调用。
- onWindowFocusChanged(boolean)  当该组件得到、失去焦点时调用。
- onAtrrachedToWindow() 当把该组件放入到某个窗口时调用。
- onDetachedFromWindow() 当把该组件从某个窗口上分离时触发的方法。
- onWindowVisibilityChanged(int): 当包含该组件的窗口的可见性发生改变时触发的方法。

###### 我这里详细讲一下onMeasure方法和onLayout方法。说得通俗一点onMeasure方法就是告诉父容器你要多大的地方。对于ViewGroup在这里需要递归的去计算每一个子视图的大小。onMeasure方法有两个参数(widthMeasureSpec,heightMeasureSpec)表示布局期望的子控件规格。等一下我放一张图就好理解了。在这里还需要知道三种测量模式:
1.EXACTLY 表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY;
2.AT_MOST 表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST;
3.UNSPECIFIED 表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见。
具体可参考<http://blog.csdn.net/dmk877/article/details/49558367>
 <pre>
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
       int modeWidth = MeasureSpec.getMode(widthMeasureSpec);   
       int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);    
       int modeHeight = MeasureSpec.getMode(heightMeasureSpec);    
       int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);    
       int resultWidth = 100;//设置一个默认宽度   
     if(modeWidth == MeasureSpec.EXACTLY){        
       resultWidth = sizeWidth;   
        }else{        
            resultWidth = Math.min(resultWidth,sizeWidth);    
     }    
        int resultHeight = 100;//设置一个默认高度    
     if(modeHeight == MeasureSpec.EXACTLY){       
         resultHeight = sizeHeight;    }
   else{        
        resultHeight = Math.min(resultHeight,sizeHeight);    
}    
//此句必须要写    
       setMeasuredDimension(resultWidth,resultHeight);}
   简单的重写onMeasure的列子</pre>
###### 接下来我就要用重写onMeasure方法来打造能适配图片的ImageView。这里有一篇个人感觉不错的适配文章<http://www.zcool.com.cn/article/ZNjI3NDQ=.html>
<pre>
public class CleverImageView extends ImageView {    
     private double scale = 3.6;//宽高比    
     public CleverImageView(Context context) {  this(context, null); }    
     public CleverImageView(Context context, AttributeSet attrs) {this(context, attrs, 0); }    
     public CleverImageView(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr); }    
    @Override    
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        
      int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);        
      int modeWidth = MeasureSpec.getMode(widthMeasureSpec);        
      int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);        
      int modeHeight = MeasureSpec.getMode(heightMeasureSpec);       
     if (modeWidth == MeasureSpec.EXACTLY && modeHeight ==   MeasureSpec.AT_MOST) {           
        sizeHeight = (int) (sizeWidth / scale + 0.5f);//这里加0.5f是为了四舍五入        
   } 
else if (modeHeight == MeasureSpec.EXACTLY && modeWidth == MeasureSpec.AT_MOST) {            
        sizeWidth = (int) (sizeHeight / scale + 0.5f);        }        
        setMeasuredDimension(sizeWidth,sizeHeight);   
 }    
    public void setScale(double scale) {        
           this.scale = scale;    
      }
  }
    其实ImageView的adjustbound属性就能解决这个问题,我这边只是拿这个举个栗子。
</pre>
###### 知道了大小之后,通过onLayout就是解决放在哪里和怎么放的问题。各位可以看一下流式布局<https://github.com/blazsolar/FlowLayout>,对这两个方法诠释的比较清楚。我这边简单的实现了一个。
<pre>
public class FlowLayoutSp extends ViewGroup {    
   public FlowLayoutSp(Context context) { super(context); }    
   public FlowLayoutSp(Context context, AttributeSet attrs) {super(context, attrs);}    
   public FlowLayoutSp(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    
   @Override    
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        
   int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);       
   int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);        
   int modeWidth = MeasureSpec.getMode(widthMeasureSpec);       
   int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
/** * 只有在内容为wrap情况下才需要记录width和height * width:总宽度 * height:总高度 *    lineWidth:行宽 * lineHeight :行高 */        
   int width = 0;       
   int height = 0;        
   int lineWidth = 0;       
   int lineHeight = 0;        
   for (int i = 0; i < getChildCount(); i++) {            
     View child = getChildAt(i);           
     measureChild(child, widthMeasureSpec, heightMeasureSpec);            
     MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();           
     int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;           
     int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;            
     lineHeight = Math.max(lineHeight, childHeight);            
lineWidth += childWidth;           
    if (lineWidth > sizeWidth) {               
        lineWidth = childWidth;               
        height += lineHeight;            
   }else{                
        width = Math.max(width, lineWidth);           
 }            
    if (i == getChildCount() - 1) {               
        width = Math.max(width, lineWidth);               
        height += lineHeight;            
    }        
}        
   setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth  : width, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight : height);    }    
   @Override    
    protected void onLayout(boolean changed, int l, int t, int r, int b) {        
      int left = 0;       
      int right = 0;        
      int top = 0;       
      int bottom = 0;        
      int lineWidth = 0;        
      int lineHeight = 0;        
      int width = getWidth();//ViewGroup的总宽       
      int height = 0;//目前的行高        
     for (int i = 0; i < getChildCount(); i++) {           
        View child = getChildAt(i);            
        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();           
        int chidWidth = child.getMeasuredWidth();            
        int childHeight = child.getMeasuredHeight();           
        lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin);            
        left = lp.leftMargin + lineWidth;            
        right = lp.rightMargin + chidWidth + lp.leftMargin + lineWidth;            
        top = lp.topMargin + height;           
        bottom = lp.bottomMargin + childHeight + lp.topMargin + height;            
        lineWidth = right;            
       if(lineWidth > width) {               
         lineWidth = 0;                
         left = lp.leftMargin + lineWidth;               
         right = lp.rightMargin + chidWidth + lp.leftMargin + lineWidth;               
         height += lineHeight;               
         top = lp.topMargin + height;              
         bottom = lp.bottomMargin + childHeight + lp.topMargin + height;                
         lineWidth = right;            
}            
  child.layout(left, top, right-lp.rightMargin, bottom-lp.bottomMargin);        
    }    
}    
@Override   
 public LayoutParams generateLayoutParams(AttributeSet attrs) {        
    return new MarginLayoutParams(getContext(), attrs);    
   }
}
</pre>
![递归.png](http://upload-images.jianshu.io/upload_images/1976633-9d9ec7706dc3075f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
> 每一个ViewGroup负责要求它的每一个孩子被绘制,每一个View负责绘制自己。

###### 2.自定义属性,即在xml设置属性或则设置style。流程如下:
- 在res/values文件下定义一个attrs.xml文件。建立declare-styleable,取一个自己想取得名字,不过最好是跟自定义控件的名字保持一致。
- 定义需要用到的各个属性,属性的类型在下方表格中给出。属性定义时可以指定多种类型值。
- 在自定义控件中获取定义的值。一定要注意最后需要回收。


![属性取值类型.png](http://upload-images.jianshu.io/upload_images/1976633-18dfbd4d803f20b4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
###### 废话不多说,上列子了。这个是一个通过继承HorizontalScrollView实现类似qq侧滑效果。注意这边的属性动画用的nineoldandroids框架的.

<pre>
public class SlideMenu extends HorizontalScrollView{    
     private int screenWidth;    
     private DisplayMetrics outMetrics;    
     private int margenRight, menuWidth;    
     private LinearLayout mWapper;    
     private ViewGroup mMenu;    
     private ViewGroup mContent;    
     private boolean first = true;    
   public SlideMenu(Context context, AttributeSet attrs) {  super(context, attrs);        
     WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);       
     outMetrics = new DisplayMetrics();        
     manager.getDefaultDisplay().getMetrics(outMetrics);        
     screenWidth = outMetrics.widthPixels;       
     margenRight = 100;   
 }    
/*     * 设置子view的宽和高 设置viewGroup的宽和高。目的是设置左边布局的一个偏移量     */    
@Override   
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        
     if (first) {            
       mWapper = (LinearLayout) getChildAt(0);            
       mMenu = (ViewGroup) mWapper.getChildAt(0);            
       mContent = (ViewGroup) mWapper.getChildAt(1);            
       menuWidth = mMenu.getLayoutParams().width = screenWidth-margenRight;            
       mContent.getLayoutParams().width = screenWidth;            
       first = false;        
}       
     super.onMeasure(widthMeasureSpec, heightMeasureSpec);    
}    
      //一开始向左滚动screenWidth - margenRight    
     @Override    
   protected void onLayout(boolean changed, int l, int t, int r, int b) {        
      super.onLayout(changed, l, t, r, b);       
       if (changed) {            
          this.scrollTo(screenWidth - margenRight, 0);        
             }    
       }    
   @SuppressLint("ClickableViewAccessibility")    
   @Override    
 public boolean onTouchEvent(MotionEvent ev) {        
      switch (ev.getAction()) {            
        case MotionEvent.ACTION_UP:               
        int scrollX = getScrollX();                
       //当滑动的距离大于menu宽度的一半自动滚动menuWidth,否则滚回起始点               
        if (scrollX >= menuWidth / 2) {                    
         //smoothScrollTo方法滚动平缓                    
        this.smoothScrollTo(menuWidth, 0);              
  } else {                    
        this.smoothScrollTo(0, 0);               
  }                
      return true;        
}       
    return super.onTouchEvent(ev);    
}    
/**     * 滚动发生时     */    
  @Override    
  protected void onScrollChanged(int l, int t, int oldl, int oldt) {        
     super.onScrollChanged(l, t, oldl, oldt);        
    //由于刚开始时向左边滚动了menuWidth,所以l的起始值为menuWidth        
    float scale = l x 1.0f / menuWidth; // 1 ~ 0        
    float rightScale = 0.7f + 0.3f x scale;        
    float leftScale = 1.0f - scale x 0.3f;       
    float leftAlpha = 0.6f + 0.4f x (1 - scale);        
    // 调用属性动画
    ViewHelper.setTranslationX(mMenu, menuWidth * scale* 0.8f);        
    ViewHelper.setPivotX(mContent, 0);        
    ViewHelper.setPivotY(mContent, getHeight() / 2);        
    ViewHelper.setScaleX(mContent, rightScale);        
    ViewHelper.setScaleY(mContent, rightScale);        
    ViewHelper.setScaleX(mMenu, leftScale);      
    ViewHelper.setScaleY(mMenu, leftScale);        
    ViewHelper.setAlpha(mMenu, leftAlpha);    
   }
}
</pre>
![侧滑.gif](http://upload-images.jianshu.io/upload_images/1976633-bcd6288ebe7ffa85.gif?imageMogr2/auto-orient/strip)

#####细心的朋友可能已经注意到上方的列子中第一个子View右边的偏移量margenRight 我是写死的。这时候吐糟声就来了“你这个很不优雅啊,我想自定义怎么办啊,我想在xml中设置怎么办啊”。客官别急,这就满足你的要求。且看下方修改后的代码。
- 首先建立declare-styleable,说白了就是定义你需要用到的属性。在这之前先在values下建立一个叫attr的文件,所有declare-styleable放在它里面
  <resources>
    <declare-styleable name="SlideMenu">
        <attr name="marginRight" format="dimension"></attr>
    </declare-styleable>
</resources>
- 接下来就是修改SlideMenu ,我这边单放修改了的代码,因为我懒
        public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.SlideMenu);
        margenRight = array.getInteger(R.styleable.SlideMenu_marginRight,(int)getResources().getDimension(R.dimen.marginRight));//我这边设置了默认偏移量是50dp
        array.recycle();//最后别忘了回收TypedArray

        WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        outMetrics = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(outMetrics);
        screenWidth = outMetrics.widthPixels;}
- 然后你就可以为所欲为了,我们来看下xml中怎么用
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
             //这边添加命名空间,名字你可以随意取,我这边取得是app。我这简单的一种写法。还有一种写法是后面跟自定义view所在的包名
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center"
       >
           <com.llf.wheelview.SlideMenu
               android:layout_width="match_parent"
               android:layout_height="match_parent"
               app:marginRight="@dimen/marginRight"
               >
           </com.llf.wheelview.SlideMenu>
       </LinearLayout>

##纯自定义
在讲这个之前先要学习一下动画和绘图
动画可以参考我在github上托管的项目,比较详细<https://github.com/liulingfeng/Animation>
绘画看我的另外一篇文章<http://www.jianshu.com/p/d92f1bebd3ab>
######学好了基本功开始学习心法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容