按照难度来排列:
1.自绘式自定义控件(完全自定义控件):继承的是view
2.组合式自定义控件:继承的是viewgroup
3.继承式自定义控件
自绘式自定义控件的定义:
自绘控件,顾名思义就是控件所展示的内容都是我们自己绘制上去的。所有的绘制操作就是在onDraw()方法里面进行的,当然我们的这个自定义控件都是View的直接子类。比如最常使用的TextView、ImageView就是View的直接子类,也可视作自绘控件,所有的绘图操作也都是在自己的onDraw()中。
自绘式自定义控件的步骤:
1.类继承View(从一个普通的类,变成一个控件)
2.复写他必须要复写的三个方法
3.在OnMeasure和Ondrawer方法,写对应的业务逻辑
自绘式自定义控件,牢记的三个方法:
1.onMeasure(int,int)测量:该方法来检查view组件及他所包含的所有子组件的大小
2.onLayout(boolean,int,int,int,int)位置:当该组件要分配其子组件的位置,大小时,调用(使用频率较少)
3.onDraw(canves)绘制:当该组件将要绘制他的内容时,回调该方法(使用频率最高,不要在此方法里做耗时操作和对象的建立)
组合式自定义控件的定义:
即组合多个原生控件来达到某些单个原生控件原本不具备的功能,这个在日常开发中应该是使用的比较多的,如基本上每个App都存在一个标题栏,而这些标题栏是可以被复用的,此时我们可以将其抽象出来组合为一个控件,这样在需要标题栏的地方直接使用该控件。(当然也可以采用include布局的方式,但是这样仅仅只是界面复用而已,功能不能复用)
组合式自定义控件的步骤:
1.类继承viewGroup下的任意自定义控件
2.复写构造方法
继承式自定义控件就是继承现有控件,对其控件的功能进行拓展。
今天主要分享一个视差特效的功能
该功能可以分为两点:
1.当ListView下拉的时候,顶部的HeaderView会有一个拉长的效果;
2. 当下拉一段距离后,ListView会复位,执行一个简单的回弹动画。
先看一下这个功能的实现效果
实现视差特效有两种方法
第一种:
解析onTouche,Action_Down,Action_Move,Action_up,业务逻辑过于复杂(这个方法不用)
第二种:
重写ListView的ouverScrollBy方法,继承式自定义控件ListView,根据用户下拉的距离,动态的修改headerView的高度
实现思路:
1.拷贝文本资源到项目中,自定义控件继承ListView
2.使用自定义控件,并往头部添加布局,设置适配器
3.使用视图树,把imageview传给我们的自定义控件
继承式自定义控件实现思路:
1.继承ListView,复写构造方法
2.复写overScrollBy方法,重点关注deltaY,isTouchEvent方法
3.暴露一个方法,去得到外界的Imageview,并测量imageview控件的高度
4.复写onTouchEvent方法
首先,创建一个自定义的类,继承ListView
public class ParallaxListView extends ListView { private ImageView iv_header; private int drawableHeight; private int orignalHeight; public ParallaxListView(Context context) { this(context, null); } public ParallaxListView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ParallaxListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public void setparallaxImage(ImageView iv_header) { this.iv_header = iv_header; //B.获取图片的原始高度 drawableHeight = iv_header.getDrawable().getIntrinsicHeight(); //C.获取Imageview控件的原始高度,以便回弹时,回弹到原始高度 orignalHeight = iv_header.getHeight(); } /** * A.滑动到ListView两端时,才会被调用 * * @param deltaY:竖直方向滑动的瞬时变化量,顶部下拉为负,顶部上拉为正 * @param isTouchEvent 是否是用户触摸拉动,true表示用户手指拉动,false是惯性 * @return */ @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { //A.通过log来验证参数的作用 System.out.println("deltaY" + deltaY); System.out.println("isTouchEvent" + isTouchEvent); //A.顶部下拉,用户触摸的操作才执行视差效果 if (deltaY < 0 &&isTouchEvent){ //A.deltaY是负值,我们要改为绝对值,累加给我们的iv_headler 高度 int newHeight = iv_header.getHeight() + Math.abs(deltaY); //B.避免图片的无限放大,使图片最大不能够超过图片本身的高度 if (newHeight<=drawableHeight){ //把新的高度值,赋值给控件,改变控件的高度 iv_header.getLayoutParams().height=newHeight; iv_header.requestLayout(); } } return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); } //C.复写触摸事件,让滑动的图片重新恢复到原有的样子 //触摸事件的监听 @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_UP: //把当前的头布局的高度恢复初始高度 int currentHeight = iv_header.getHeight(); //属性动画,改变高度的值,把我们当前头布局的高度,改为原始时的高度 final ValueAnimator animator = ValueAnimator.ofInt(currentHeight, orignalHeight); //动画更新的监听 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { //获取动画执行过程中的分度值 float fraction = animator.getAnimatedFraction(); //获取中间的值,并赋给控件新高度,可以使控件平稳回弹的效果 Integer animatedValue = (Integer) animator.getAnimatedValue(); //让新的高度值生效 iv_header.getLayoutParams().height=animatedValue; iv_header.requestLayout(); } }); //动画回弹效果,值越大,回弹越厉害 animator.setInterpolator(new OvershootInterpolator(2)); //设置动画的执行时间 animator.setDuration(1000); //动画执行 animator.start(); break; } return super.onTouchEvent(event); } }
接下来是 XML布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.demo.MainActivity"> <com.example.demo.ParallaxListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/plv"></com.example.demo.ParallaxListView> </RelativeLayout>
创建一个文本工具类,这里呢,我们借助一下梁山109个好汉
public class cheeses { public static final String[] NAMES = new String[]{"宋江", "卢俊义", "吴用", "公孙胜", "关胜", "林冲", "秦明", "呼延灼", "花荣", "柴进", "李应", "朱仝", "鲁智深", "武松", "董平", "张清", "杨志", "徐宁", "索超", "戴宗", "刘唐", "李逵", "史进", "穆弘", "雷横", "李俊", "阮小二", "张横", "阮小五", " 张顺", "阮小七", "杨雄", "石秀", "解珍", " 解宝", "燕青", "朱武", "黄信", "孙立", "宣赞", "郝思文", "韩滔", "彭玘", "单廷珪", "魏定国", "萧让", "裴宣", "欧鹏", "邓飞", " 燕顺", "杨林", "凌振", "蒋敬", "吕方", "郭 盛", "安道全", "皇甫端", "王英", "扈三娘", "鲍旭", "樊瑞", "孔明", "孔亮", "项充", "李衮", "金大坚", "马麟", "童威", "童猛", "孟康", "侯健", "陈达", "杨春", "郑天寿", "陶宗旺", "宋清", "乐和", "龚旺", "丁得孙", "穆春", "曹正", "宋万", "杜迁", "薛永", "施恩", "周通", "李忠", "杜兴", "汤隆", "邹渊", "邹润", "朱富", "朱贵", "蔡福", "蔡庆", "李立", "李云", "焦挺", "石勇", "孙新", "顾大嫂", "张青", "孙二娘", " 王定六", "郁保四", "白胜", "时迁", "段景柱", "李思儒"}; }
新建一个XML布局,里面实现头布局的图片
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv_header" android:layout_width="match_parent" android:layout_height="160dp" android:src="@drawable/a2" android:scaleType="centerCrop" /> </LinearLayout>
最后我们在MainActivity中添加一下头布局
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //A.view final ParallaxListView plv = (ParallaxListView) findViewById(R.id.plv); //B.listview添加一个头布局 View headerView = View.inflate(this, R.layout.layout_head, null); plv.addHeaderView(headerView); final ImageView iv_header = (ImageView) headerView.findViewById(R.id.iv_header); //等view界面全部绘制完毕的时候,去得到已经绘制完控件的宽和高,查一下这个方法,并做一个笔记 iv_header.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { //宽和高已经测量完毕 plv.setparallaxImage(iv_header); //释放资源 iv_header.getViewTreeObserver().removeGlobalOnLayoutListener(this); } }); //B.使用ListView的ArrayAfapter,添加文本的item plv.setAdapter(new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,cheeses.NAMES)); } }
好了,这就可以实现自定义View的视差特效!
这里在插一个方法getViewTreeObserver().addOnGlobalLayoutListener的详解
我们知道在oncreate中View.getWidth和View.getHeight无法获得一个view的高度和宽度,这是因为View组件布局要在onResume回调后完成。所以现在需要使用getViewTreeObserver().addOnGlobalLayoutListener()来获得宽度或者高度。这是获得一个view的宽度和高度的方法之一。
OnGlobalLayoutListener 是ViewTreeObserver的内部类,当一个视图树的布局发生改变时,可以被ViewTreeObserver监听到,
这是一个注册监听视图树的观察者(observer),在视图树的全局事件改变时得到通知。ViewTreeObserver不能直接实例化,而是通过getViewTreeObserver()获得。
除了OnGlobalLayoutListener ,ViewTreeObserver还有如下内部类:
interface ViewTreeObserver.OnGlobalFocusChangeListener
当在一个视图树中的焦点状态发生改变时,所要调用的回调函数的接口类
interface ViewTreeObserver.OnGlobalLayoutListener
当在一个视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变时,所要调用的回调函数的接口类
interface ViewTreeObserver.OnPreDrawListener
当一个视图树将要绘制时,所要调用的回调函数的接口类
interface ViewTreeObserver.OnScrollChangedListener
当一个视图树中的一些组件发生滚动时,所要调用的回调函数的接口类
interface ViewTreeObserver.OnTouchModeChangeListener
当一个视图树的触摸模式发生改变时,所要调用的回调函数的接口类
其中,我们可以利用
OnGlobalLayoutListener
来获得一个视图的真实高度。