现在的位置: 首页 > 综合 > 正文

Fragment兼容手机与平板实现3D画廊和下拉列表(附源码)

2013年03月28日 ⁄ 综合 ⁄ 共 24825字 ⁄ 字号 评论关闭

我们先来看看功能实现后的效果图

手机效果,点击3D画廊和英雄列表,分别跳转到其显示界面


车机上的效果,点击3D画廊和英雄列表,在右边会更新界面,左边的选项一直保留


由于本人没有android平板所以用车机来代替了,不过原理都是一样的

来看看实现的思路和步骤:

如果一个程序要根据屏幕大小实现2种界面效果,那么肯定也要给程序配置2种布局了


Layout中的activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal"
    tools:context=".MainActivity" >

    <fragment
        android:id="@+id/menu_fragment"
        android:name="huahua.myfragment.MenuFragment"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        />

</LinearLayout>

Layout-Large中的activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal"
    android:baselineAligned="false"
    tools:context=".MainActivity"
    >

    <fragment
        android:id="@+id/left_fragment"
        android:name="huahua.myfragment.MenuFragment"
        android:layout_width="0dip"
        android:layout_height="fill_parent"
        android:layout_weight="1"
        />
    
    <FrameLayout 
        android:id="@+id/details_layout"
        android:layout_width="0dip"
        android:layout_height="fill_parent"
        android:layout_weight="3"
        ></FrameLayout>

这里用到了动态加载布局,首先MainActivity中只调用 setContentView(R.layout.activity_main) 

package huahua.myfragment;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	}

}

表明当前的Activity想加载activity_main这个布局文件。而Android系统又会根据当前的运行环境判断程序是否运行在大屏幕设备上,如果运行在大屏幕设备上,就加载layout-large目录下的activity_main.xml,否则就默认加载layout目录下的activity_main.xml。

下面我们来看看实现红色背景的MenuFragment

package huahua.myfragment;

import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;

/**
 * 菜单的Fragment,用于显示菜单界面内容,以及处理菜单界面的点击事件。
 */
public class MenuFragment extends Fragment implements OnItemClickListener {

	/**
	 * 菜单界面中只包含了一个ListView。
	 */
	private ListView menuList;

	/**
	 * ListView的适配器。
	 */
	private ArrayAdapter<String> adapter;

	/**
	 * 用于填充ListView的数据,这里就简单只用了两条数据。
	 */
	private String[] menuItems = { "3D画廊", "英雄列表" };

	/**
	 * 是否是双页模式。如果一个Activity中包含了两个Fragment,就是双页模式。
	 */
	private boolean isTwoPane;

	/**
	 * 当Activity和Fragment建立关联时,初始化适配器中的数据。
	 */
	@Override
	public void onAttach(Activity activity) {
		super.onAttach(activity);
		adapter = new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, menuItems);
	}

	/**
	 * 加载menu_fragment布局文件,为ListView绑定了适配器,并设置了监听事件。
	 */
	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		View view = inflater.inflate(R.layout.menu_fragment, container, false);
		menuList = (ListView) view.findViewById(R.id.menu_list);
		menuList.setAdapter(adapter);
		menuList.setOnItemClickListener(this);
		return view;
	}

	/**
	 * 当Activity创建完毕后,尝试获取一下布局文件中是否有details_layout这个元素,如果有说明当前
	 * 是双页模式,如果没有说明当前是单页模式。
	 */
	@Override
	public void onActivityCreated(Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);
		if (getActivity().findViewById(R.id.details_layout) != null) {
			isTwoPane = true;
		} else {
			isTwoPane = false;
		}
	}

	/**
	 * 处理ListView的点击事件,会根据当前是否是双页模式进行判断。如果是双页模式,则会动态添加Fragment。
	 * 如果不是双页模式,则会打开新的Activity。
	 */
	@Override
	public void onItemClick(AdapterView<?> arg0, View view, int index, long arg3) {
		if (isTwoPane) {
			Fragment fragment = null;
			if (index == 0) {
				fragment = new GalleryFragment();
			} else if (index == 1) {
				fragment = new listFragment();
			}
			getFragmentManager().beginTransaction().replace(R.id.details_layout, fragment).commit();
		} else {
			Intent intent = null;
			if (index == 0) {
				intent = new Intent(getActivity(), GalleryActivity.class);
			} else if (index == 1) {
				intent = new Intent(getActivity(), ListActivity.class);
			}
			startActivity(intent);
		}
	}

}

在这个类的onCreateView方法中加载了menu_fragment这个布局,布局里面包含了一个ListView,然后我们对这个ListView填充了两个简单的数据 "3D画廊" 和 "英雄列表" 。又在onActivityCreated方法中做了一个判断,如果Activity的布局中包含了details_layout这个元素,那么当前就是双页模式,否则就是单页模式。onItemClick方法则处理了ListView的点击事件,发现如果当前是双页模式,就动态往details_layout中添加Fragment,如果当前是单页模式,就直接打开新的Activity。


下面把实现2种Gallery的布局文件贴出来

gallery_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sound_fragment"
    android:name="huahua.myfragment.GalleryFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

</fragment>


gallery_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#3982F4"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:textSize="16sp" />
    
    <huahua.myfragment.GalleryView        
        android:id="@+id/mygallery"
        android:spacing="20dp"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:unselectedAlpha="128"
        android:layout_below="@id/tvTitle"
        android:layout_marginTop="10dip" />

</RelativeLayout>

可以看到在gallery_activity.xml布局文件引用了GalleryFragment,这样写的好处就是,以后我们只需要在GalleryFragment.java中修改代码,GalleryActivity界面就会跟着自动改变了,因为它所有的代码都是从GalleryFragment中引用过来的

GalleryActivity.java

package huahua.myfragment;

import android.app.Activity;
import android.os.Bundle;

/**
 * 3D画廊界面的Activity,加入了gallery_activity的布局。
 */
public class GalleryActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.gallery_activity);
	}

}

很简单,只加载了gallery_activity.xml布局文件

GalleryFragment.java

package huahua.myfragment;

import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemSelectedListener;

/**
 * 3D画廊界面的Fragment,加载了gallery_fragment布局。
 * ImageAdapter中主要做了图片的倒影效果以及创建了对原始图片和倒影的显示区域。
 * GalleryView中主要做了对图片的旋转和缩放操作,根据图片的屏幕中的位置对其进行旋转缩放操作
 */
public class GalleryFragment extends Fragment {
	private TextView tvTitle; 	
	private GalleryView gallery; 	
	private ImageAdapter adapter;
	private View mMainView;
	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		mMainView = inflater.inflate(R.layout.gallery_fragment, container, false);
		
		try {
			initRes();
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return mMainView;
	}
	
	private void initRes() throws IllegalArgumentException, IllegalAccessException{
		tvTitle = (TextView) mMainView.findViewById(R.id.tvTitle);
		gallery = (GalleryView) mMainView.findViewById(R.id.mygallery);

		adapter = new ImageAdapter(getActivity()); 	
		adapter.createReflectedImages();//创建倒影效果
		
		gallery.setFadingEdgeLength(0);
//		gallery.setSpacing(-100); //图片之间的间距
		
		gallery.setAdapter(adapter);
		
		gallery.setOnItemSelectedListener(new OnItemSelectedListener() {
			@Override
			public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
				tvTitle.setText(adapter.titles.get(position));
			}

			@Override
			public void onNothingSelected(AdapterView<?> parent) {
			}
		});

		gallery.setOnItemClickListener(new OnItemClickListener() {			// 设置点击事件监听
			@Override
			public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
				Toast.makeText(getActivity(), "图片 " + (position+1) + " 选中", Toast.LENGTH_SHORT).show();
			}
		});
	}

}

这个就是3D画廊界面的Fragment,具体的3D效果实现需要用到GalleryView和ImageAdapter

GalleryView.java

package huahua.myfragment;

import android.content.Context;
import android.graphics.Camera;
import android.graphics.Matrix;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Transformation;
import android.widget.Gallery;
import android.widget.ImageView;

public class GalleryView extends Gallery 
{
	private Camera mCamera = new Camera();//相机类
	private int mMaxRotationAngle = 45;		// 最大旋转角度 45
	private int mMaxZoom = -120;//最大缩放值
	private int mCoveflowCenter;//半径值

	public GalleryView(Context context) 
	{
		super(context);
		 //支持转换 ,执行getChildStaticTransformation方法
		this.setStaticTransformationsEnabled(true);
	}

	public GalleryView(Context context, AttributeSet attrs) 
	{
		super(context, attrs);
		this.setStaticTransformationsEnabled(true);
	}

	public GalleryView(Context context, AttributeSet attrs, int defStyle) 
	{
		super(context, attrs, defStyle);
		this.setStaticTransformationsEnabled(true);
	}

	public int getMaxRotationAngle() 
	{
		return mMaxRotationAngle;
	}

	public void setMaxRotationAngle(int maxRotationAngle) 
	{
		mMaxRotationAngle = maxRotationAngle;
	}

	public int getMaxZoom() 
	{
		return mMaxZoom;
	}

	public void setMaxZoom(int maxZoom) 
	{
		mMaxZoom = maxZoom;
	}

	/** 获取Gallery的中心x */
	private int getCenterOfCoverflow() 
	{
		return (getWidth() - getPaddingLeft() - getPaddingRight()) / 2 + getPaddingLeft();
	}

	/** 获取View的中心x */
	private static int getCenterOfView(View view) {
		return view.getLeft() + view.getWidth() / 2;
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) 
	{
		mCoveflowCenter = getCenterOfCoverflow();
		super.onSizeChanged(w, h, oldw, oldh);
	} 

	//控制gallery中每个图片的旋转(重写的gallery中方法)
	@Override
	protected boolean getChildStaticTransformation(View child, Transformation trans) 
	{
		 //取得当前子view的半径值
		final int childCenter = getCenterOfView(child);
		final int childWidth = child.getWidth();
		
		  //旋转角度
		int rotationAngle = 0;

		//重置转换状态
		trans.clear();
		
		 //设置转换类型
		trans.setTransformationType(Transformation.TYPE_BOTH);		// alpha 和 matrix 都变换

		 //如果图片位于中心位置不需要进行旋转
		if (childCenter == mCoveflowCenter) 
		{	// 正中间的childView
			transformImageBitmap((ImageView) child, trans, 0);	
		} 
		else 
		{		// 两侧的childView
			 //根据图片在gallery中的位置来计算图片的旋转角度
			rotationAngle = (int) ( ( (float) (mCoveflowCenter - childCenter) / childWidth ) * mMaxRotationAngle );
			
			//如果旋转角度绝对值大于最大旋转角度返回(-mMaxRotationAngle或mMaxRotationAngle;)
			if (Math.abs(rotationAngle) > mMaxRotationAngle) 
			{
				rotationAngle = (rotationAngle < 0) ? -mMaxRotationAngle : mMaxRotationAngle;
			}
			transformImageBitmap((ImageView) child, trans, rotationAngle);
		}

		return true;
	}

	private void transformImageBitmap(ImageView child, Transformation trans, int rotationAngle) 
	{
		//对效果进行保存
		mCamera.save();
		
		final Matrix imageMatrix = trans.getMatrix();
		 //图片高度
		final int imageHeight = child.getLayoutParams().height;
		 //图片宽度
		final int imageWidth = child.getLayoutParams().width;
		 //返回旋转角度的绝对值
		final int rotation = Math.abs(rotationAngle);

		// 在Z轴上正向移动camera的视角,实际效果为放大图片; 如果在Y轴上移动,则图片上下移动; X轴上对应图片左右移动。
		mCamera.translate(0.0f, 0.0f, -20.0f);

		// As the angle of the view gets less, zoom in
		if (rotation < mMaxRotationAngle)
		{
			float zoomAmount = (float) (mMaxZoom + (rotation * 1.0));
			mCamera.translate(0.0f, 0.0f, zoomAmount);
		}

		mCamera.rotateY(rotationAngle);		// rotationAngle 为正,沿y轴向内旋转; 为负,沿y轴向外旋转
		
		mCamera.getMatrix(imageMatrix);
		imageMatrix.preTranslate(-(imageWidth / 2), -(imageHeight / 2));
		imageMatrix.postTranslate((imageWidth / 2), (imageHeight / 2));
		
		mCamera.restore();
	}
}

这个类中实现了3D效果


ImageAdapter.java

package huahua.myfragment;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Shader.TileMode;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;

public class ImageAdapter extends BaseAdapter 
{
	private ImageView[] mImages;		// 保存倒影图片的数组

	private Context mContext;
	public List<Map<String, Object>> list;
	

	public ArrayList<Integer> imgs=new ArrayList<Integer>(); 
	public ArrayList<String> titles=new ArrayList<String>(); 

	public ImageAdapter(Context c) throws IllegalArgumentException, IllegalAccessException 
	{
		this.mContext = c;
		list = new ArrayList<Map<String, Object>>();
		int count =0;
		
        //用反射机制来获取资源中的图片ID
        Field[] fields = R.drawable.class.getDeclaredFields();  
        for (Field field : fields)  
        {  
            if(field.getName().contains("hero"))//只在画廊中显示包含"hero"的图片
            {     
                int index=field.getInt(R.drawable.class);  
                imgs.add(index);  
                
                HashMap<String, Object> map = new HashMap<String, Object>();
    			map.put("image", index);
    			list.add(map);
    			
    			titles.add("英雄"+ ++count );
            }  
            
        }  
        
		mImages = new ImageView[list.size()];
	}

	/** 反射倒影 */
	public boolean createReflectedImages() 
	{
		final int reflectionGap = 4;
		final int Height = 200;
		int index = 0;
		for (Map<String, Object> map : list) {
			Integer id = (Integer) map.get("image");
			// 获取原始图片  //返回原图解码之后的bitmap对象
			Bitmap originalImage = BitmapFactory.decodeResource(mContext.getResources(), id);	
			int width = originalImage.getWidth();
			int height = originalImage.getHeight();
			float scale = Height / (float)height;
			
			Matrix sMatrix = new Matrix();
			sMatrix.postScale(scale, scale);
			Bitmap miniBitmap = Bitmap.createBitmap(originalImage, 0, 0,
					originalImage.getWidth(), originalImage.getHeight(), sMatrix, true);
			
			//是否原图片数据,节省内存
			originalImage.recycle();

			int mwidth = miniBitmap.getWidth();
			int mheight = miniBitmap.getHeight();
			Matrix matrix = new Matrix();
			matrix.preScale(1, -1);			// 图片矩阵变换(从低部向顶部的倒影)
			Bitmap reflectionImage = Bitmap.createBitmap(miniBitmap, 0, mheight/2, mwidth, mheight/2, matrix, false);	// 截取原图下半部分
			Bitmap bitmapWithReflection = Bitmap.createBitmap(mwidth, (mheight + mheight / 2), Config.ARGB_8888);			// 创建倒影图片(高度为原图3/2)

			Canvas canvas = new Canvas(bitmapWithReflection);	// 绘制倒影图(原图 + 间距 + 倒影)
			canvas.drawBitmap(miniBitmap, 0, 0, null);		// 绘制原图
			Paint paint = new Paint();
			canvas.drawRect(0, mheight, mwidth, mheight + reflectionGap, paint);		// 绘制原图与倒影的间距
			canvas.drawBitmap(reflectionImage, 0, mheight + reflectionGap, null);	// 绘制倒影图

			paint = new Paint();
			  /**
		       * 参数一:为渐变起初点坐标x位置,
		       * 参数二:为y轴位置,
		       * 参数三和四:分辨对应渐变终点,
		       * 最后参数为平铺方式,
		       * 这里设置为镜像Gradient是基于Shader类,所以我们通过Paint的setShader方法来设置这个渐变
		       */
			LinearGradient shader = new LinearGradient(0, miniBitmap.getHeight(), 0, bitmapWithReflection.getHeight()
					+ reflectionGap, 0x70ffffff, 0x00ffffff, TileMode.CLAMP);
			paint.setShader(shader);	// 线性渐变效果
			paint.setXfermode(new PorterDuffXfermode(Mode.DST_IN));		// 倒影遮罩效果
			canvas.drawRect(0, mheight, mwidth, bitmapWithReflection.getHeight() + reflectionGap, paint);		// 绘制倒影的阴影效果

			ImageView imageView = new ImageView(mContext);
			imageView.setImageBitmap(bitmapWithReflection);		// 设置倒影图片
			imageView.setLayoutParams(new GalleryView.LayoutParams((int)(width * scale),
					(int)(mheight * 3 / 2.0 + reflectionGap)));
			imageView.setScaleType(ScaleType.MATRIX);
			mImages[index++] = imageView;
		}
		return true;
	}

	@Override
	public int getCount() {
		return imgs.size();
	}

	@Override
	public Object getItem(int position) {
		return mImages[position];
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		return mImages[position];		// 显示倒影图片(当前获取焦点)
	}

	public float getScale(boolean focused, int offset) {
		return Math.max(0, 1.0f / (float) Math.pow(2, Math.abs(offset)));
	}

}

这个类获取到图片,并实现了倒影的效果


同样对于英雄列表Activity和Fragment实现的思路和3D画廊的实现思路一致

也是通过修改listFragment.java中代码,listActivity界面就会跟着自动改变了

唯一的区别就是一个是列表的界面,一个是画廊的界面

其中还有一个很重要的文件是RefreshableView.java,这个是自定义下拉刷新控件

package huahua.myfragment;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.View.OnTouchListener;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

/**
 * 可进行下拉刷新的自定义控件。
 */
public class RefreshableView extends LinearLayout implements OnTouchListener {

	/**
	 * 下拉状态
	 */
	public static final int STATUS_PULL_TO_REFRESH = 0;

	/**
	 * 释放立即刷新状态
	 */
	public static final int STATUS_RELEASE_TO_REFRESH = 1;

	/**
	 * 正在刷新状态
	 */
	public static final int STATUS_REFRESHING = 2;

	/**
	 * 刷新完成或未刷新状态
	 */
	public static final int STATUS_REFRESH_FINISHED = 3;

	/**
	 * 下拉头部回滚的速度
	 */
	public static final int SCROLL_SPEED = -20;

	/**
	 * 一分钟的毫秒值,用于判断上次的更新时间
	 */
	public static final long ONE_MINUTE = 60 * 1000;

	/**
	 * 一小时的毫秒值,用于判断上次的更新时间
	 */
	public static final long ONE_HOUR = 60 * ONE_MINUTE;

	/**
	 * 一天的毫秒值,用于判断上次的更新时间
	 */
	public static final long ONE_DAY = 24 * ONE_HOUR;

	/**
	 * 一月的毫秒值,用于判断上次的更新时间
	 */
	public static final long ONE_MONTH = 30 * ONE_DAY;

	/**
	 * 一年的毫秒值,用于判断上次的更新时间
	 */
	public static final long ONE_YEAR = 12 * ONE_MONTH;

	/**
	 * 上次更新时间的字符串常量,用于作为SharedPreferences的键值
	 */
	private static final String UPDATED_AT = "updated_at";

	/**
	 * 下拉刷新的回调接口
	 */
	private PullToRefreshListener mListener;

	/**
	 * 用于存储上次更新时间
	 */
	private SharedPreferences preferences;

	/**
	 * 下拉头的View
	 */
	private View header;

	/**
	 * 需要去下拉刷新的ListView
	 */
	private ListView listView;

	/**
	 * 刷新时显示的进度条
	 */
	private ProgressBar progressBar;

	/**
	 * 指示下拉和释放的箭头
	 */
	private ImageView arrow;

	/**
	 * 指示下拉和释放的文字描述
	 */
	private TextView description;

	/**
	 * 上次更新时间的文字描述
	 */
	private TextView updateAt;

	/**
	 * 下拉头的布局参数
	 */
	private MarginLayoutParams headerLayoutParams;

	/**
	 * 上次更新时间的毫秒值
	 */
	private long lastUpdateTime;

	/**
	 * 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分
	 */
	private int mId = -1;

	/**
	 * 下拉头的高度
	 */
	private int hideHeaderHeight;

	/**
	 * 当前处理什么状态,可选值有STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH,
	 * STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED
	 */
	private int currentStatus = STATUS_REFRESH_FINISHED;;

	/**
	 * 记录上一次的状态是什么,避免进行重复操作
	 */
	private int lastStatus = currentStatus;

	/**
	 * 手指按下时的屏幕纵坐标
	 */
	private float yDown;

	/**
	 * 在被判定为滚动之前用户手指可以移动的最大值。
	 */
	private int touchSlop;

	/**
	 * 是否已加载过一次layout,这里onLayout中的初始化只需加载一次
	 */
	private boolean loadOnce;

	/**
	 * 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉
	 */
	private boolean ableToPull;

	/**
	 * 下拉刷新控件的构造函数,会在运行时动态添加一个下拉头的布局。
	 * 
	 * @param context
	 * @param attrs
	 */
	public RefreshableView(Context context, AttributeSet attrs) {
		super(context, attrs);
		preferences = PreferenceManager.getDefaultSharedPreferences(context);
		header = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh, null, true);
		progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);
		arrow = (ImageView) header.findViewById(R.id.arrow);
		description = (TextView) header.findViewById(R.id.description);
		updateAt = (TextView) header.findViewById(R.id.updated_at);
		touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
		refreshUpdatedAtValue();
		setOrientation(VERTICAL);
		addView(header, 0);
	}

	/**
	 * 进行一些关键性的初始化操作,比如:将下拉头向上偏移进行隐藏,给ListView注册touch事件。
	 */
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		if (changed && !loadOnce) {
			hideHeaderHeight = -header.getHeight();
			headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();
			headerLayoutParams.topMargin = hideHeaderHeight;
			listView = (ListView) getChildAt(1);
			listView.setOnTouchListener(this);
			loadOnce = true;
		}
	}

	/**
	 * 当ListView被触摸时调用,其中处理了各种下拉刷新的具体逻辑。
	 */
	@Override
	public boolean onTouch(View v, MotionEvent event) {
		setIsAbleToPull(event);
		if (ableToPull) {
			switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN:
				yDown = event.getRawY();
				break;
			case MotionEvent.ACTION_MOVE:
				float yMove = event.getRawY();
				int distance = (int) (yMove - yDown);
				// 如果手指是下滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件
				if (distance <= 0 && headerLayoutParams.topMargin <= hideHeaderHeight) {
					return false;
				}
				if (distance < touchSlop) {
					return false;
				}
				if (currentStatus != STATUS_REFRESHING) {
					if (headerLayoutParams.topMargin > 0) {
						currentStatus = STATUS_RELEASE_TO_REFRESH;
					} else {
						currentStatus = STATUS_PULL_TO_REFRESH;
					}
					// 通过偏移下拉头的topMargin值,来实现下拉效果
					headerLayoutParams.topMargin = (distance / 2) + hideHeaderHeight;
					header.setLayoutParams(headerLayoutParams);
				}
				break;
			case MotionEvent.ACTION_UP:
			default:
				if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
					// 松手时如果是释放立即刷新状态,就去调用正在刷新的任务
					new RefreshingTask().execute();
				} else if (currentStatus == STATUS_PULL_TO_REFRESH) {
					// 松手时如果是下拉状态,就去调用隐藏下拉头的任务
					new HideHeaderTask().execute();
				}
				break;
			}
			// 时刻记得更新下拉头中的信息
			if (currentStatus == STATUS_PULL_TO_REFRESH
					|| currentStatus == STATUS_RELEASE_TO_REFRESH) {
				updateHeaderView();
				// 当前正处于下拉或释放状态,要让ListView失去焦点,否则被点击的那一项会一直处于选中状态
				listView.setPressed(false);
				listView.setFocusable(false);
				listView.setFocusableInTouchMode(false);
				lastStatus = currentStatus;
				// 当前正处于下拉或释放状态,通过返回true屏蔽掉ListView的滚动事件
				return true;
			}
		}
		return false;
	}

	/**
	 * 给下拉刷新控件注册一个监听器。
	 * 
	 * @param listener
	 *            监听器的实现。
	 * @param id
	 *            为了防止不同界面的下拉刷新在上次更新时间上互相有冲突, 请不同界面在注册下拉刷新监听器时一定要传入不同的id。
	 */
	public void setOnRefreshListener(PullToRefreshListener listener, int id) {
		mListener = listener;
		mId = id;
	}

	/**
	 * 当所有的刷新逻辑完成后,记录调用一下,否则你的ListView将一直处于正在刷新状态。
	 */
	public void finishRefreshing() {
		currentStatus = STATUS_REFRESH_FINISHED;
		preferences.edit().putLong(UPDATED_AT + mId, System.currentTimeMillis()).commit();
		new HideHeaderTask().execute();
	}

	/**
	 * 根据当前ListView的滚动状态来设定 {@link #ableToPull}
	 * 的值,每次都需要在onTouch中第一个执行,这样可以判断出当前应该是滚动ListView,还是应该进行下拉。
	 * 
	 * @param event
	 */
	private void setIsAbleToPull(MotionEvent event) {
		View firstChild = listView.getChildAt(0);
		if (firstChild != null) {
			int firstVisiblePos = listView.getFirstVisiblePosition();
			if (firstVisiblePos == 0 && firstChild.getTop() == 0) {
				if (!ableToPull) {
					yDown = event.getRawY();
				}
				// 如果首个元素的上边缘,距离父布局值为0,就说明ListView滚动到了最顶部,此时应该允许下拉刷新
				ableToPull = true;
			} else {
				if (headerLayoutParams.topMargin != hideHeaderHeight) {
					headerLayoutParams.topMargin = hideHeaderHeight;
					header.setLayoutParams(headerLayoutParams);
				}
				ableToPull = false;
			}
		} else {
			// 如果ListView中没有元素,也应该允许下拉刷新
			ableToPull = true;
		}
	}

	/**
	 * 更新下拉头中的信息。
	 */
	private void updateHeaderView() {
		if (lastStatus != currentStatus) {
			if (currentStatus == STATUS_PULL_TO_REFRESH) {
				description.setText(getResources().getString(R.string.pull_to_refresh));
				arrow.setVisibility(View.VISIBLE);
				progressBar.setVisibility(View.GONE);
				rotateArrow();
			} else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
				description.setText(getResources().getString(R.string.release_to_refresh));
				arrow.setVisibility(View.VISIBLE);
				progressBar.setVisibility(View.GONE);
				rotateArrow();
			} else if (currentStatus == STATUS_REFRESHING) {
				description.setText(getResources().getString(R.string.refreshing));
				progressBar.setVisibility(View.VISIBLE);
				arrow.clearAnimation();
				arrow.setVisibility(View.GONE);
			}
			refreshUpdatedAtValue();
		}
	}

	/**
	 * 根据当前的状态来旋转箭头。
	 */
	private void rotateArrow() {
		float pivotX = arrow.getWidth() / 2f;
		float pivotY = arrow.getHeight() / 2f;
		float fromDegrees = 0f;
		float toDegrees = 0f;
		if (currentStatus == STATUS_PULL_TO_REFRESH) {
			fromDegrees = 180f;
			toDegrees = 360f;
		} else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
			fromDegrees = 0f;
			toDegrees = 180f;
		}
		RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
		animation.setDuration(100);
		animation.setFillAfter(true);
		arrow.startAnimation(animation);
	}

	/**
	 * 刷新下拉头中上次更新时间的文字描述。
	 */
	private void refreshUpdatedAtValue() {
		lastUpdateTime = preferences.getLong(UPDATED_AT + mId, -1);
		long currentTime = System.currentTimeMillis();
		long timePassed = currentTime - lastUpdateTime;
		long timeIntoFormat;
		String updateAtValue;
		if (lastUpdateTime == -1) {
			updateAtValue = getResources().getString(R.string.not_updated_yet);
		} else if (timePassed < 0) {
			updateAtValue = getResources().getString(R.string.time_error);
		} else if (timePassed < ONE_MINUTE) {
			updateAtValue = getResources().getString(R.string.updated_just_now);
		} else if (timePassed < ONE_HOUR) {
			timeIntoFormat = timePassed / ONE_MINUTE;
			String value = timeIntoFormat + "分钟";
			updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
		} else if (timePassed < ONE_DAY) {
			timeIntoFormat = timePassed / ONE_HOUR;
			String value = timeIntoFormat + "小时";
			updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
		} else if (timePassed < ONE_MONTH) {
			timeIntoFormat = timePassed / ONE_DAY;
			String value = timeIntoFormat + "天";
			updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
		} else if (timePassed < ONE_YEAR) {
			timeIntoFormat = timePassed / ONE_MONTH;
			String value = timeIntoFormat + "个月";
			updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
		} else {
			timeIntoFormat = timePassed / ONE_YEAR;
			String value = timeIntoFormat + "年";
			updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
		}
		updateAt.setText(updateAtValue);
	}

	/**
	 * 正在刷新的任务,在此任务中会去回调注册进来的下拉刷新监听器。
	 */
	class RefreshingTask extends AsyncTask<Void, Integer, Void> {

		@Override
		protected Void doInBackground(Void... params) {
			int topMargin = headerLayoutParams.topMargin;
			while (true) {
				topMargin = topMargin + SCROLL_SPEED;
				if (topMargin <= 0) {
					topMargin = 0;
					break;
				}
				publishProgress(topMargin);
				sleep(10);
			}
			currentStatus = STATUS_REFRESHING;
			publishProgress(0);
			if (mListener != null) {
				mListener.onRefresh();
			}
			return null;
		}

		@Override
		protected void onProgressUpdate(Integer... topMargin) {
			updateHeaderView();
			headerLayoutParams.topMargin = topMargin[0];
			header.setLayoutParams(headerLayoutParams);
		}

	}

	/**
	 * 隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏。
	 */
	class HideHeaderTask extends AsyncTask<Void, Integer, Integer> {

		@Override
		protected Integer doInBackground(Void... params) {
			int topMargin = headerLayoutParams.topMargin;
			while (true) {
				topMargin = topMargin + SCROLL_SPEED;
				if (topMargin <= hideHeaderHeight) {
					topMargin = hideHeaderHeight;
					break;
				}
				publishProgress(topMargin);
				sleep(10);
			}
			return topMargin;
		}

		@Override
		protected void onProgressUpdate(Integer... topMargin) {
			headerLayoutParams.topMargin = topMargin[0];
			header.setLayoutParams(headerLayoutParams);
		}

		@Override
		protected void onPostExecute(Integer topMargin) {
			headerLayoutParams.topMargin = topMargin;
			header.setLayoutParams(headerLayoutParams);
			currentStatus = STATUS_REFRESH_FINISHED;
		}
	}

	/**
	 * 使当前线程睡眠指定的毫秒数。
	 * 
	 * @param time
	 *            指定当前线程睡眠多久,以毫秒为单位
	 */
	private void sleep(int time) {
		try {
			Thread.sleep(time);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 下拉刷新的监听器,使用下拉刷新的地方应该注册此监听器来获取刷新回调。
	 */
	public interface PullToRefreshListener {

		/**
		 * 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 你可以不必另开线程来进行耗时操作。
		 */
		void onRefresh();

	}

}

好了,关键的思路和代码就在上面了,关于很多细节都写了注释,有兴趣的朋友可以下载源码

源码下载地址

最后在这里推荐2位Android大牛的博客

http://blog.csdn.net/guolin_blog

http://www.cnblogs.com/mythou


抱歉!评论已关闭.