打造QQ空间头部视差ListView
来源:互联网 发布:linux mysql开始连接 编辑:程序博客网 时间:2024/05/23 18:43
QQ空间相信大家都用过,是否觉得它的下拉刷新很酷呢?今天就来自己实现这个控件。
本文主要是讲思想和一些api,想要使用此效果到项目中的同学请点击这里带动画的下拉刷新RecyclerView
效果图:
对实现过程不感兴趣的童鞋可以直接到文章底部粘帖代码,代码中有详细注释。
要实现这样的效果,需要重写ListView控件,并在ListView中处理下拉事件。
首先我们进行ListView最基础的操作,就是设置适配器显示头部布局和一个列表出来,这些操作相信大家都会写,直接贴出代码:
activity_main.xml
<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" tools:context=".MainActivity"> <com.example.sch.headzoomlistviewdemo.HeadZoomListView android:id="@+id/list_view" android:layout_width="match_parent" android:layout_height="wrap_content" /></RelativeLayout>
上面这个是Activity的内容布局,其中包含一个自定义的ListView控件com.example.sch.headzoomlistviewdemo.HeadZoomListView
。
list_view_header.xml
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:id="@+id/iv_hander" android:scaleType="centerCrop" android:layout_width="match_parent" android:layout_height="162dp" android:src="@mipmap/banner1" /></RelativeLayout>
此布局做为ListView的头部,里面只包含一个ImageView,可以看到给ImageView设置了android:scaleType="centerCrop"
属性,表示按比例扩大此ImageView的图片资源的size居中显示,使得图片长(宽)等于或大于View的长(宽) ,但是此属性只有在ImageView使用android:src=""
属性或者setImageBitmap()
或者setImageResource()
方法设置图片时才有效,使用background
设置背景是无效的。
android:scaleType
的各种值的含义可以参考 ImageView.ScaleType设置图解
MainActivity.java
package com.example.sch.headzoomlistviewdemo;import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.view.LayoutInflater;import android.view.View;import android.widget.ArrayAdapter;import android.widget.ImageView;import android.widget.ListView;import java.util.ArrayList;import java.util.List;/** * Created by shichaohui on 2015/7/31 0031. */public class MainActivity extends AppCompatActivity { private HeadZoomListView mListView; private View headerView; private List<String> datas; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); mListView = (HeadZoomListView) this.findViewById(R.id.list_view); headerView = LayoutInflater.from(this).inflate(R.layout.list_view_header, null); mListView.addHeaderView(headerView); mListView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, datas)); } /** * 初始化数据 */ private void initData() { datas = new ArrayList<>(); for (int i = 0; i < 3; i++) { datas.add("条目 " + (i + 1)); } }}
接着就是我们的重头戏自定义ListView了,首先回顾我们需要实现的效果:
- 在顶部继续下拉时头部拉伸;
- 拉伸之后手指上推减小拉伸高度;
- 拉伸时即时更改背景图的透明度;
- 松手后自动弹回原位置。
根据要实现的效果,我们可以推出需要的参数如下:
private ImageView headerImage;private int headerImageHeight = -1; // 默认高度private int headerImageMaxHeight = -1; // 最大高度private float scaleRatio = 1.5f; // 最大拉伸比例private int headerImageScaleHeight = -1; // 被拉伸的高度private float headerImageMinAlpha = 0.5f; // 拉伸到最高时头部的透明度private long durationMillis = 1000; // 头部恢复动画的执行时间
由于ListView中并不能直接获取Header,所以我们需要定义一个函数,由调用者传入头部的背景ImageView,并计算相关属性:
/** * 设置头部图片 * * @param headerImage 头部中的背景ImageView */public void setHeaderImage(ImageView headerImage) { this.headerImage = headerImage; headerImageHeight = headerImage.getHeight(); headerImageMaxHeight = (int) (headerImageHeight * scaleRatio); // 防止第一次拉伸的时候headerImage.getLayoutParams().height = 0 headerImage.getLayoutParams().height = headerImageHeight;}
为了计算的准确性,我们需要在View显示出来后才调用setHeaderImage()
,因此需要重写MainActivity的onWindowFocusChanged()
方法:
@Overridepublic void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); mListView.setHeaderImage((ImageView) headerView.findViewById(R.id.iv_hander));}
为了增加扩展性,还增加以下几个方法:
/** * 设置头部的最大拉伸倍率,默认1.5f * * @param scaleRatio 头部的最大拉伸倍率,必须大于1,小于1则默认为1.5f */public void setScaleRatio(float scaleRatio) { this.scaleRatio = scaleRatio;}/** * 设置拉伸到最高时头部的透明度,默认0.5f * * @param headerImageMinAlpha 拉伸到最高时头部的透明度,0.0~1.0 */public void setHeaderImageMinAlpha(float headerImageMinAlpha) { this.headerImageMinAlpha = headerImageMinAlpha;}/** * 设置头部恢复动画的执行时间,默认1000毫秒 * * @param durationMillis 头部恢复动画的执行时间,单位:毫秒 */public void setHeaderImageDurationMillis(long durationMillis) { this.durationMillis = durationMillis;}
接下来重写ListView的overScrollBy()
方法处理下拉/上拉过度事件:
@Overrideprotected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { // deltaY为拉伸过度时每毫秒拉伸的距离,正数表示向上拉伸多度,负数表示向下拉伸过度 if (deltaY < 0 && headerImage.getLayoutParams().height < headerImageMaxHeight || deltaY > 0 && headerImage.getLayoutParams().height > headerImageHeight) { // 修改宽高 headerImage.getLayoutParams().height -= deltaY; // 重新设置View的宽高 headerImage.requestLayout(); } return true;}
当ListView到达边界并继续拉的时候(这里称为”下拉/上拉过度”)就会触发此方法,其中参数deltaY
表示每毫秒拉动的距离,下拉时此参数是负数,上拉过度时是正数。
因此,满足条件deltaY < 0 && headerImage.getLayoutParams().height < headerImageMaxHeight
(下拉且没有到达最大高度)的时候,需要增大headerImage的宽度,但是此时deltaY
是负数,因此使用”-=”修改高度。条件deltaY > 0 && headerImage.getLayoutParams().height > headerImageHeight
表示上拉过度且已被拉伸的时候,需要减小headerImage的宽度,但是此时deltaY
是正数,因此也使用”-=”修改高度。
headerImage.requestLayout();
会重新测量View的宽高,不调用此方法上面的修改也就不会更新到界面上。
实现下拉一段高度后上推减小headerImage的拉伸高度效果,需要重写onScrollChanged()
方法,重写onTouchEvent()
也可以,只是太难控制,且效果不太好:
@Overrideprotected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (headerImage == null) { return; } View view = (View) headerImage.getParent(); // 上推的时候减小高度至默认高度 if (view.getTop() < 0 && headerImage.getLayoutParams().height > headerImageHeight) { headerImage.getLayoutParams().height += view.getTop(); // 重新计算尺寸布局 view.layout(view.getLeft(), 0, view.getRight(), view.getBottom()); headerImage.requestLayout(); }}
view.layout(view.getLeft(), 0, view.getRight(), view.getBottom());
遍历视图树,重新测量并设置头部的高度和子布局的位置。
重新测量的时候,View使用requestLayout()
方法,ViewGroup使用layout()
方法,layout()
方法中的四个参数前两个表示ViewGroup左上角坐标和右下角坐标。
接着实现松手时的动画:
@Overridepublic boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (headerImage.getLayoutParams().height > headerImageHeight) { // 使用动画恢复默认高度 headerImage.clearAnimation(); headerImage.startAnimation(new ResetAnimaton()); return true; } } return super.onTouchEvent(ev);}/** * 自定义恢复时的动画 */class ResetAnimaton extends Animation { public ResetAnimaton() { setDuration(durationMillis); // 计算开始动画时的拉伸高度 headerImageScaleHeight = headerImage.getLayoutParams().height - headerImageHeight; } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { // interpolatedTime从动画开始到结束,由0.0~1.0 if (headerImage.getLayoutParams().height - headerImageHeight > 0) { // 计算新高度 headerImage.getLayoutParams().height -= headerImageScaleHeight * interpolatedTime; // 计算新拉伸高度 headerImageScaleHeight -= headerImageScaleHeight * interpolatedTime; // 重新布局 headerImage.requestLayout(); } }}
最后加入拉伸时透明度的变化:
@Overrideprotected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); ... updateHeaderAlpha();}/** * 更新头部的透明度 */private void updateHeaderAlpha() { // 当前拉伸高度 int scallHeight = headerImage.getLayoutParams().height - headerImageHeight; if (scallHeight > 0) { // 新的透明度(1 - 当前拉伸高度 / 最大拉伸高度 * (1 - 目标透明度)) headerImage.setAlpha(1 - (float) scallHeight / (headerImageMaxHeight - headerImageHeight) * (1 - headerImageMinAlpha)); }}
贴完整代码
MainActivity.java
package com.example.sch.headzoomlistviewdemo;import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.view.LayoutInflater;import android.view.View;import android.widget.ArrayAdapter;import android.widget.ImageView;import android.widget.ListView;import java.util.ArrayList;import java.util.List;/** * Created by shichaohui on 2015/7/31 0031. */public class MainActivity extends AppCompatActivity { private HeadZoomListView mListView; private View headerView; private List<String> datas; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); mListView = (HeadZoomListView) this.findViewById(R.id.list_view); headerView = LayoutInflater.from(this).inflate(R.layout.list_view_header, null); mListView.addHeaderView(headerView); mListView.setScaleRatio(2.0f); mListView.setHeaderImageDurationMillis(800); mListView.setHeaderImageMinAlpha(0.3f); mListView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, datas)); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); mListView.setHeaderImage((ImageView) headerView.findViewById(R.id.iv_hander)); } /** * 初始化数据 */ private void initData() { datas = new ArrayList<>(); for (int i = 0; i < 3; i++) { datas.add("条目 " + (i + 1)); } }}
HeadZoomListView.java
package com.example.sch.headzoomlistviewdemo;import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.view.animation.Animation;import android.view.animation.Transformation;import android.widget.ImageView;import android.widget.ListView;/** * 下拉头部缩放ListView * <br/> * Created by shichaohui on 2015/7/31 0031. */public class HeadZoomListView extends ListView { private ImageView headerImage; private int headerImageHeight = -1; // 默认高度 private int headerImageMaxHeight = -1; // 最大高度 private int headerImageScaleHeight = -1; // 被拉伸的高度 private float scaleRatio = 1.5f; // 最大拉伸比例 private float headerImageMinAlpha = 0.5f; // 拉伸到最高时头部的透明度 private long durationMillis = 1000; // 头部恢复动画的执行时间 public HeadZoomListView(Context context) { super(context); } public HeadZoomListView(Context context, AttributeSet attrs) { super(context, attrs); } public HeadZoomListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 设置头部图片 * * @param headerImage 头部中的背景ImageView */ public void setHeaderImage(ImageView headerImage) { this.headerImage = headerImage; headerImageHeight = headerImage.getHeight(); headerImageMaxHeight = (int) (headerImageHeight * scaleRatio); // 防止第一次拉伸的时候headerImage.getLayoutParams().height = 0 headerImage.getLayoutParams().height = headerImageHeight; } /** * 设置头部的最大拉伸倍率,默认1.5f * * @param scaleRatio 头部的最大拉伸倍率,必须大于1,小于1则默认为1.5f */ public void setScaleRatio(float scaleRatio) { this.scaleRatio = scaleRatio; } /** * 设置拉伸到最高时头部的透明度,默认0.5f * * @param headerImageMinAlpha 拉伸到最高时头部的透明度,0.0~1.0 */ public void setHeaderImageMinAlpha(float headerImageMinAlpha) { this.headerImageMinAlpha = headerImageMinAlpha; } /** * 设置头部恢复动画的执行时间,默认1000毫秒 * * @param durationMillis 头部恢复动画的执行时间,单位:毫秒 */ public void setHeaderImageDurationMillis(long durationMillis) { this.durationMillis = durationMillis; } @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { // deltaY为拉伸过度时每毫秒拉伸的距离,正数表示向上拉伸多度,负数表示向下拉伸过度 if (deltaY < 0 && headerImage.getLayoutParams().height < headerImageMaxHeight || deltaY > 0 && headerImage.getLayoutParams().height > headerImageHeight) { // 修改宽高 headerImage.getLayoutParams().height -= deltaY; // 重新设置View的宽高 headerImage.requestLayout(); } return true; } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (headerImage == null) { return; } View view = (View) headerImage.getParent(); // 上推的时候减小高度至默认高度 if (view.getTop() < 0 && headerImage.getLayoutParams().height > headerImageHeight) { headerImage.getLayoutParams().height += view.getTop(); // 重新计算尺寸布局 view.layout(view.getLeft(), 0, view.getRight(), view.getBottom()); headerImage.requestLayout(); } updateHeaderAlpha(); } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (headerImage.getLayoutParams().height > headerImageHeight) { // 使用动画恢复默认高度 headerImage.clearAnimation(); headerImage.startAnimation(new ResetAnimaton()); return true; } } return super.onTouchEvent(ev); } /** * 更新头部的透明度 */ private void updateHeaderAlpha() { // 当前拉伸高度 int scallHeight = headerImage.getLayoutParams().height - headerImageHeight; if (scallHeight > 0) { // 新的透明度(1 - 当前拉伸高度 / 最大拉伸高度 * (1 - 目标透明度)) headerImage.setAlpha(1 - (float) scallHeight / (headerImageMaxHeight - headerImageHeight) * (1 - headerImageMinAlpha)); } } /** * 自定义恢复时的动画 */ class ResetAnimaton extends Animation { public ResetAnimaton() { setDuration(durationMillis); // 计算开始动画时的拉伸高度 headerImageScaleHeight = headerImage.getLayoutParams().height - headerImageHeight; } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { // interpolatedTime从动画开始到结束,由0.0~1.0 if (headerImage.getLayoutParams().height - headerImageHeight > 0) { // 计算新高度 headerImage.getLayoutParams().height -= headerImageScaleHeight * interpolatedTime; // 计算新拉伸高度 headerImageScaleHeight -= headerImageScaleHeight * interpolatedTime; // 重新布局 headerImage.requestLayout(); } } }}
布局文件在文章开头有贴出,这里就不重复了。
END
欢迎评论吐槽……
- 打造QQ空间头部视差ListView
- 打造QQ空间头部视差ListView
- ListView头部视差效果
- Android ListView头部视差控件
- 仿QQ空间打造可拉伸头部组件
- 仿QQ空间之打造个性化可拉伸头部控件
- 仿QQ空间之打造个性化可拉伸头部控件
- 彷QQ空间图片拉伸展示—头部视差效果Parallax
- QQ空间头部图片可拉伸的ListView效果
- ListView的头部视差效果的实现
- 头部视觉视差(仿QQ控件下拉效果)
- 打造QQ个性化可拉伸头部控件
- 仿QQ空间下拉头部缩放
- 仿QQ空间头部下拉放大控件
- 自定义控件--头部视差
- 头部视差的实现
- android打造仿qq控件可拉伸头部控件
- 手工打造一个QQ空间备份工具
- PCIe SSD KVM IO性能调优
- poj 1328 Radar Installation 【贪心】
- gcc提高程序性能的几个参数
- windows cmd批处理
- VirtualBox安装VBoxGuestAdditions增强功能
- 打造QQ空间头部视差ListView
- JDBC 连接数据库实例(MySQL为例)
- c语言详解+例子1
- javascript显示农历
- solr增量更新的字段
- weblogic部署应用,访问控制台就报内存溢出
- HTML DOM节点
- ubuntu x64安装jd-gui
- LeetCode_234Palindrome Linked List