先来看看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源码位置