先来看看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帮我们计算出来的平移值去做平移,就达到了我们看到的自动平移的效果。
整个核心的类差不多就是这样了,现在贴出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源码位置