利用Recycleview水平平移并自动挪动Item位置(仿Instagram效果)

时间:2021-10-29 12:54:01

先来看看Instagram在编辑图片水平挪动的过程中的效果图

利用Recycleview水平平移并自动挪动Item位置(仿Instagram效果)

这个效果分两个,一个是水平滑动的效果,另一个是当我点击靠边的某个选项的时候,若这个选项与它靠近的边之间还隔着一个没有完全显示的选项,会自动平移位置,将这个没有完全显示的选项显示出来,或者当点击的选项就是最边上的选项,它也会自动移动将它之前(或者之后)的选项移动出来。


知道效果后,我们就要用代码实现这个效果,按照庖丁解牛一步步达到我们的功能。

首先第一步是做一个能水平滑动的view,最早之前android只提供了竖直滑动的ListView(大家都很熟悉吧),这两年又提供了一个Recycleview可以*控制水平还是竖直滚动,同时它的适配器的写法也比ListView更简单些,Recylerview用法网上的例子很多了,大家自己百度下。我们的这个demo就是自定义一个继承Recycleview的View,在本身就有水平滚动功能的基础上加入自动平移的功能。所以写贴出关键代码的自定义View的代码


package com.jc.demo.recylerview;

import android.content.Context;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.Interpolator;
import android.widget.Scroller;

public class AutoAdjustRecylerView extends RecyclerView {
	private final String TAG = "AutoAdjustRecylerView";
	private Scroller mScroller = null;
	private int mLastx = 0;
	//用于设置自动平移时候的速度
	private float mPxPerMillsec = 0;
	private AutoAdjustItemClickListener mListener = null;
	
	public AutoAdjustRecylerView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
		initData(context);
	}

	public AutoAdjustRecylerView(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
		initData(context);
	}

	public AutoAdjustRecylerView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		// TODO Auto-generated constructor stub
		initData(context);
	}
	
	private void initData(Context context){
		mScroller = new Scroller(context, new Interpolator() {
			public float getInterpolation(float t) {
			      return t;
			  }
		});
	}
	
	public void setScroller(Scroller scroller){
		if(mScroller != scroller){
			mScroller = scroller; 
		}
	}
	
	@Override
	public void computeScroll() {
		// TODO Auto-generated method stub
		super.computeScroll();
		if(mScroller != null){
			if (mScroller.computeScrollOffset())//如果mScroller没有调用startScroll,这里将会返回false。    
		    {      
		        Log.d(TAG, "getCurrX = " +  mScroller.getCurrX());    
		        scrollBy(mLastx - mScroller.getCurrX(), 0);
		        mLastx = mScroller.getCurrX();
		        //继续让系统重绘    
		        postInvalidate();  
		     } 
		}
	}
	
	public AutoAdjustItemClickListener getItemClickListener() {
		return mListener;
	}

	public void setItemClickListener(AutoAdjustItemClickListener listener) {
		this.mListener = listener;
	}
	
	public float getPxPerMillsec() {
		return mPxPerMillsec;
	}

	public void setPxPerMillsec(float pxPerMillsec) {
		this.mPxPerMillsec = pxPerMillsec;
	}
	
	public void checkAutoAdjust(int position){
		int childcount = getChildCount();
		//获取可视范围内的选项的头尾位置
		int firstvisiableposition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
		int lastvisiableposition = ((LinearLayoutManager) getLayoutManager()).findLastVisibleItemPosition();
		Log.d(TAG, "childcount:" + childcount + " position:"+ position +" firstvisiableposition:" + firstvisiableposition
				+ " lastvisiableposition" + lastvisiableposition);
		if(position == (firstvisiableposition + 1) || position == firstvisiableposition){
			//当前位置需要向右平移
			leftScrollBy(position, firstvisiableposition);
		}
		else if(position == (lastvisiableposition - 1) || position == lastvisiableposition){
			//当前位置需要向做平移
			rightScrollBy(position, lastvisiableposition);
		}
	}
	
	private void leftScrollBy(int position, int firstvisiableposition){
		View leftChild = getChildAt(0);
		if(leftChild != null){
			int leftx = leftChild.getLeft();
			Log.d(TAG, "leftChild left:" + leftx);
			int startleft = leftx;
			int endleft = position == firstvisiableposition?leftChild.getWidth():0;
			Log.d(TAG, "startleft:" + startleft + " endleft" + endleft);
			autoAdjustScroll(startleft, endleft);
		}
	}
	
	
	private void rightScrollBy(int position, int lastvisiableposition){
		int childcount = getChildCount();
		View rightChild = getChildAt(childcount - 1);
		if(rightChild != null){
			int rightx = rightChild.getRight();
			int dx = rightx - getWidth();
			Log.d(TAG, "rightChild right:" + rightx + " dx:" + dx);
			int startright = dx;
			int endright = position == lastvisiableposition?-1 * rightChild.getWidth():0;
			Log.d(TAG,"startright:" + startright + " endright:" + endright);
			autoAdjustScroll(startright, endright);
		}
	}
	
	/**
	 * 
	 * @param start 滑动起始位置
	 * @param end 滑动结束位置
	 */
	private void autoAdjustScroll(int start, int end){
		int duration = 0;
        if(mPxPerMillsec != 0){
        	duration = (int)((Math.abs(end - start)/mPxPerMillsec));
        }
        Log.d(TAG, "duration:" + duration);
		mLastx = start;
		mScroller.startScroll(start, 0, end - start, 0, duration);
		postInvalidate();
	}
	
	public abstract class AbstractAutoAdjustViewHolder extends ViewHolder implements OnClickListener{
		
		private final static String TAG = "AutoAdjustViewHolder";
		
		public AbstractAutoAdjustViewHolder(View view) {
			super(view);
			view.setOnClickListener(this);
			initView(view);
		}
		
		protected abstract void initView(View view);
		
		/**
		 * 点击监听
		 */
		@Override
		public void onClick(View v) {
			//单击选项的时候判断是否需要移动
			checkAutoAdjust(getPosition());
			if(mListener != null){
				mListener.onItemClick(v,getPosition());
			}
		}
	}
}

首先我们看这个自定义View代码最底下有个AbstractAutoAdjustViewHolder抽象类,看到ViewHolder大家应该都知道它的作用啥吧,由于Recycleview没有ItemClick事件,因此我们必须自己添加了,所以有了这个抽象类,用来给每个Item的View设置OnClick事件,然后在这个OnClick事件里去判断是否需要平移以及将click事件回调出去,这里有一个接口用于回调点击事件

package com.jc.demo.recylerview;

import android.view.View;

public interface AutoAdjustItemClickListener {
	public void onItemClick(View view,int postion);
}

当我们点击了Item的时候,就触发了checkAutoAdjust方法去判断当前点击点的位置在整个可视范围所处的位置,代码91和95行分别判断了点击位置是否处于最左边,临近最左边或者最右边,临近最右边,然后计算好要平移的起始和结束位置,并最终在133行的autoAdjustScroll方法里进行滑动。滑动的方式用比较常用的Scroller的方式,当我们调用了Scroller的startScroller方法后postInvalidate重绘整个view,重绘过程会调用到computeScroll()方法(代码53行),然后在这个方法里取出scroller帮我们计算出来的平移值去做平移,就达到了我们看到的自动平移的效果。

利用Recycleview水平平移并自动挪动Item位置(仿Instagram效果)


整个核心的类差不多就是这样了,现在贴出UI部分的三个类,一个是ViewHolder继承了我们刚刚的AbstractAutoAdjustViewHolder



package com.jc.demo;

import android.view.View;
import android.widget.ImageView;

import com.jc.demo.recylerview.AutoAdjustRecylerView;
import com.jc.demo.recylerview.AutoAdjustRecylerView.AbstractAutoAdjustViewHolder;


public class TestViewHolder extends AbstractAutoAdjustViewHolder{
	
	public ImageView mImageView;
	
	public TestViewHolder(AutoAdjustRecylerView autoAdjustRecylerView, View view) {
		autoAdjustRecylerView.super(view);
		// TODO Auto-generated constructor stub
	}

	@Override
	protected void initView(View view) {
		// TODO Auto-generated method stub
		mImageView = (ImageView) view.findViewById(R.id.item_iv);
	}
	
	
}

里面的Item布局为

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:padding="10.0dip"
    android:orientation="vertical" >
    
    <ImageView 
        android:id="@+id/item_iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
       />
    
</LinearLayout>

要使用自定义view我们得做个适配器Adapter

package com.jc.demo;


import com.jc.demo.recylerview.AutoAdjustRecylerView;

import android.support.v7.widget.RecyclerView.Adapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;


public class TestAdapter extends Adapter<TestViewHolder>{

	private int[] mDatas;
	private AutoAdjustRecylerView mAutoAdjustRecylerView = null;

    public TestAdapter(AutoAdjustRecylerView autoAdjustRecylerView, int[] datas) {
        this.mAutoAdjustRecylerView = autoAdjustRecylerView;
    	mDatas = datas;
    }

    @Override
    public int getItemCount() {
        return mDatas.length;
    }

	@Override
	public void onBindViewHolder(TestViewHolder holder, int position) {
		// TODO Auto-generated method stub
		holder.mImageView.setImageResource(mDatas[position]);
	}

	@Override
	public TestViewHolder onCreateViewHolder(ViewGroup parent, int arg1) {
		// TODO Auto-generated method stub
		View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
        TestViewHolder testViewHolder = new TestViewHolder(mAutoAdjustRecylerView, itemView);
		return testViewHolder;
	}

}

然后就是主界面和主界面布局

package com.jc.demo;


import com.jc.demo.recylerview.AutoAdjustItemClickListener;
import com.jc.demo.recylerview.AutoAdjustRecylerView;

import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.util.Log;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationSet;
import android.view.animation.RotateAnimation;
import android.view.animation.ScaleAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;


public class AutoAdjustActivity extends Activity implements AutoAdjustItemClickListener{
	private final String TAG = "AutoAdjustActivity";
	private AutoAdjustRecylerView mRecyclerView = null;
	final int[] mIds = {R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.d,
	        R.drawable.e, R.drawable.f, R.drawable.g, R.drawable.h,
	        R.drawable.i, R.drawable.j, R.drawable.k, R.drawable.l,
	        R.drawable.m, R.drawable.n, R.drawable.o, R.drawable.p,
	        R.drawable.q};
	final int[] mIdsUpper = {R.drawable.aa, R.drawable.bb, R.drawable.cc, R.drawable.dd,
	        R.drawable.ee, R.drawable.ff, R.drawable.gg, R.drawable.hh,
	        R.drawable.ii, R.drawable.jj, R.drawable.kk, R.drawable.ll,
	        R.drawable.mm, R.drawable.nn, R.drawable.oo, R.drawable.pp,
	        R.drawable.qq};
	private TextView mTvText = null;
	private ImageView mImageView1 = null;
	private ImageView mImageView2 = null;
	private ImageView mImageView3 = null;
	private ImageView mImageView4 = null;
	private ImageView mImageView5 = null;
	private int mClickPosition = 0;
	private ScaleAnimation mScaleAni = null;
	private RotateAnimation mRotateAni = null;
	private AnimationSet mAnimationSet = null;
	private AlphaAnimation mAlphaAnimation = null;
	
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTvText = (TextView)findViewById(R.id.tv_text);
        mImageView1 = (ImageView)findViewById(R.id.iv_pic1);
        mImageView2 = (ImageView)findViewById(R.id.iv_pic2);
        mImageView3 = (ImageView)findViewById(R.id.iv_pic3);
        mImageView4 = (ImageView)findViewById(R.id.iv_pic4);
        mImageView5 = (ImageView)findViewById(R.id.iv_pic5);
        mRecyclerView = (AutoAdjustRecylerView) findViewById(R.id.recyclerView);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        linearLayoutManager.setOrientation(LinearLayout.HORIZONTAL);
        mRecyclerView.setLayoutManager(linearLayoutManager);
        mRecyclerView.setItemClickListener(this);
        mRecyclerView.setPxPerMillsec(0.3f);
        TestAdapter adapter = new TestAdapter(mRecyclerView, mIds);
        mRecyclerView.setAdapter(adapter);
        initData();
    }
    
    private void initData(){
    	mScaleAni = new ScaleAnimation(1.0f, 0.5f, 1.0f, 0.5f,50,50); //Scale Animation  
		mScaleAni.setRepeatCount(0);  
		mScaleAni.setRepeatMode(Animation.REVERSE);  
		  
		mRotateAni = new RotateAnimation(0, 180, 50, 50); //Rotate Animation  
		mRotateAni.setRepeatMode(Animation.REVERSE);
		mRotateAni.setStartOffset(150);
		mAnimationSet = new AnimationSet(false);   //Animation Set  
		mAnimationSet.addAnimation(mScaleAni);  
		mAnimationSet.addAnimation(mRotateAni);
		mAnimationSet.setAnimationListener(new AnimationListener() {
			
			@Override
			public void onAnimationStart(Animation arg0) {
				Log.d(TAG, "onAnimationStart");
			}

			@Override
			public void onAnimationEnd(Animation arg0) {
				mImageView1.setBackgroundResource(mIds[mClickPosition]);
				mImageView2.setBackgroundResource(mIds[mClickPosition]);
				mImageView3.setBackgroundResource(mIds[mClickPosition]);
				mImageView4.setBackgroundResource(mIds[mClickPosition]);
				mImageView5.setBackgroundResource(mIds[mClickPosition]);
			}

			@Override
			public void onAnimationRepeat(Animation arg0) {
			}
		});
		mAnimationSet.setDuration(300);
		mAlphaAnimation = new AlphaAnimation(1.0f, 0f);
		mAlphaAnimation.setAnimationListener(new AnimationListener() {
			
			@Override
			public void onAnimationStart(Animation arg0) {
				
			}

			@Override
			public void onAnimationRepeat(Animation arg0) {
				
			}
			
			@Override
			public void onAnimationEnd(Animation arg0) {
				mTvText.setText("TEST" + (mClickPosition + 1));
			}
		});
		mAlphaAnimation.setDuration(1000);
    }
    
	@Override
	public void onItemClick(View view, final int position) {
		Log.d(TAG, "position:" + position);
		mClickPosition = position;
		mAnimationSet.setStartOffset(0);
		mImageView1.startAnimation(mAnimationSet);
		mImageView2.startAnimation(mAnimationSet);
		mImageView3.startAnimation(mAnimationSet);
		mImageView4.startAnimation(mAnimationSet);
		mImageView5.startAnimation(mAnimationSet);
		mAlphaAnimation.setStartOffset(0);
		mTvText.startAnimation(mAlphaAnimation);
	}

}

<LinearLayout 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:orientation="vertical" 
    android:background="#e5e5e6"
     >
    <LinearLayout 
          android:layout_width="match_parent"
		  android:layout_height="wrap_content"
		  android:orientation="vertical" 
		        >
		    <TextView 
		        android:id="@+id/tv_text"
		        android:layout_width="wrap_content"
			    android:layout_height="wrap_content"
			    android:text="TEST1"
			    android:layout_gravity="center_horizontal"
			    android:textSize="24sp"
			    android:textColor="@android:color/black"
		        />
		   <ImageView 
			    android:id="@+id/iv_pic1"
			    android:layout_width="wrap_content"
			    android:layout_height="wrap_content"
			    android:background="@drawable/a"
			    android:layout_gravity="center_horizontal"
			    />
		   <ImageView 
			    android:id="@+id/iv_pic2"
			    android:layout_width="wrap_content"
			    android:layout_height="wrap_content"
			    android:background="@drawable/a"
			    android:layout_gravity="center_horizontal"
			    />
		    <ImageView 
			    android:id="@+id/iv_pic3"
			    android:layout_width="wrap_content"
			    android:layout_height="wrap_content"
			    android:background="@drawable/a"
			    android:layout_gravity="center_horizontal"
			    />
		     <ImageView 
			    android:id="@+id/iv_pic4"
			    android:layout_width="wrap_content"
			    android:layout_height="wrap_content"
			    android:background="@drawable/a"
			    android:layout_gravity="center_horizontal"
			    />
		     <ImageView 
			    android:id="@+id/iv_pic5"
			    android:layout_width="wrap_content"
			    android:layout_height="wrap_content"
			    android:background="@drawable/a"
			    android:layout_gravity="center_horizontal"
			    />
    </LinearLayout>
    <com.jc.demo.recylerview.AutoAdjustRecylerView 
        android:id="@+id/recyclerView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
</LinearLayout>

Demo源码位置