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 = ; i < ; i++) {
datas.add("條目 " + (i + ));
}
}
}
接着就是我們的重頭戲自定義ListView了,首先回顧我們需要實作的效果:
- 在頂部繼續下拉時頭部拉伸;
- 拉伸之後手指上推減小拉伸高度;
- 拉伸時即時更改背景圖的透明度;
- 松手後自動彈回原位置。
根據要實作的效果,我們可以推出需要的參數如下:
private ImageView headerImage;
private int headerImageHeight = -; // 預設高度
private int headerImageMaxHeight = -; // 最大高度
private float scaleRatio = f; // 最大拉伸比例
private int headerImageScaleHeight = -; // 被拉伸的高度
private float headerImageMinAlpha = f; // 拉伸到最高時頭部的透明度
private long durationMillis = ; // 頭部恢複動畫的執行時間
由于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()
方法:
@Override
public 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()
方法處理下拉/上拉過度事件:
@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 < && headerImage.getLayoutParams().height < headerImageMaxHeight
|| deltaY > && 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()
也可以,隻是太難控制,且效果不太好:
@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() < && headerImage.getLayoutParams().height > headerImageHeight) {
headerImage.getLayoutParams().height += view.getTop();
// 重新計算尺寸布局
view.layout(view.getLeft(), , view.getRight(), view.getBottom());
headerImage.requestLayout();
}
}
view.layout(view.getLeft(), 0, view.getRight(), view.getBottom());
周遊視圖樹,重新測量并設定頭部的高度和子布局的位置。
重新測量的時候,View使用
requestLayout()
方法,ViewGroup使用
layout()
方法,
layout()
方法中的四個參數前兩個表示ViewGroup左上角坐标和右下角坐标。
接着實作松手時的動畫:
@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);
}
/**
* 自定義恢複時的動畫
*/
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 > ) {
// 計算新高度
headerImage.getLayoutParams().height -= headerImageScaleHeight * interpolatedTime;
// 計算新拉伸高度
headerImageScaleHeight -= headerImageScaleHeight * interpolatedTime;
// 重新布局
headerImage.requestLayout();
}
}
}
最後加入拉伸時透明度的變化:
@Override
protected 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 > ) {
// 新的透明度(1 - 目前拉伸高度 / 最大拉伸高度 * (1 - 目标透明度))
headerImage.setAlpha( - (float) scallHeight
/ (headerImageMaxHeight - headerImageHeight) * ( - 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(f);
mListView.setHeaderImageDurationMillis();
mListView.setHeaderImageMinAlpha(f);
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 = ; i < ; i++) {
datas.add("條目 " + (i + ));
}
}
}
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 = -; // 預設高度
private int headerImageMaxHeight = -; // 最大高度
private int headerImageScaleHeight = -; // 被拉伸的高度
private float scaleRatio = f; // 最大拉伸比例
private float headerImageMinAlpha = f; // 拉伸到最高時頭部的透明度
private long durationMillis = ; // 頭部恢複動畫的執行時間
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 < && headerImage.getLayoutParams().height < headerImageMaxHeight
|| deltaY > && 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() < && headerImage.getLayoutParams().height > headerImageHeight) {
headerImage.getLayoutParams().height += view.getTop();
// 重新計算尺寸布局
view.layout(view.getLeft(), , 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 > ) {
// 新的透明度(1 - 目前拉伸高度 / 最大拉伸高度 * (1 - 目标透明度))
headerImage.setAlpha( - (float) scallHeight
/ (headerImageMaxHeight - headerImageHeight) * ( - 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 > ) {
// 計算新高度
headerImage.getLayoutParams().height -= headerImageScaleHeight * interpolatedTime;
// 計算新拉伸高度
headerImageScaleHeight -= headerImageScaleHeight * interpolatedTime;
// 重新布局
headerImage.requestLayout();
}
}
}
}
布局檔案在文章開頭有貼出,這裡就不重複了。
END
歡迎評論吐槽……