查看: 446|回复: 0

[Android教程] Android使用ListView实现滚轮的动画效果实例

发表于 2017-11-28 08:00:01

之前收到一个需求,需要把一个数据展示列表页面做成像滚轮那样的动画效果:中间最大然后向上下两端逐渐缩小。我想了想iOS那边自带滚轮组件,安卓得自己去实现,目前网上仿ios的滚轮组件的也有一些,但是感觉不适合我,我的要求没那么复杂,于是决定自己动手去实现一下。

动手前先分析一下应该怎么做,归根到底只是要实现缩放效果,由中间向两边变小,当一个item越接近中间就放大,越远离中间就缩小。那么可以通过先获取ListView的中点,然后获取当前可视的所有item跟ListView的中点的垂直距离计算出一个比例,然后将item的大小根据这个比例进行缩放,各个item跟ListView的中点的垂直距离不同,计算出来的比例也就不同,然后每次滚动的时候都计算比例然后进行缩放,这样应该就能实现我们想要的效果了。

因为一开始我的列表展示就是用ListView做的,有其他效果在里面,也不方便换其他组件,所以依然还是用ListView实现就好了。由于我们是每次一滚动都要进行缩放,ListView有提供一个OnScrollListener,它的onScroll方法每次一开始滚动就会调用,所以我们选择重写它。当它被调用的时候,我们就开始获取ListView中点,然后开始计算每个item的距离进行缩放.

  1. /**
  2. * 中点的Y坐标
  3. */
  4. private float centerY = 0f;
  5. @Override
  6. public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
  7. //计算中点
  8. centerY = getHeight()/2;
  9. //判断中点的有效性
  10. if(centerY <= 0){
  11. return;
  12. }
  13. //开始对当前显示的View进行缩放
  14. for(int i = 0; i < visibleItemCount; i++){
  15. //获取item
  16. View temp_view = getChildAt(i);
  17. //计算item的中点Y坐标
  18. float itemY = temp_view.getBottom()-(temp_view.getHeight()/2);
  19. //计算离中点的距离
  20. float distance = centerY;
  21. if(itemY > centerY){
  22. distance = itemY - centerY;
  23. }else{
  24. distance = centerY - itemY;
  25. }
  26. //根据距离进行缩放
  27. temp_view.setScaleY(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
  28. temp_view.setScaleX(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
  29. //根据距离改变透明度
  30. temp_view.setAlpha(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
  31. }
  32. }
复制代码

后面不想加demo了,所以直接上整个ListView的代码吧

  1. /**
  2. * 模仿滚轮动画缩放的ListView
  3. * Created by xu on 2017/3/3.
  4. */
  5. public class XuListView extends ListView implements AbsListView.OnScrollListener {
  6. private static final String TAG = "XuListView";
  7. /**
  8. * 中点的Y坐标
  9. */
  10. private float centerY = 0f;
  11. public XuListView(Context context, AttributeSet attrs) {
  12. super(context, attrs);
  13. //设置一个滚动监听
  14. setOnScrollListener(this);
  15. }
  16. @Override
  17. public void onScrollStateChanged(AbsListView view, int scrollState) {
  18. }
  19. @Override
  20. public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
  21. //计算中点
  22. centerY = getHeight()/2;
  23. //判断中点的有效性
  24. if(centerY <= 0){
  25. return;
  26. }
  27. //开始对当前显示的View进行缩放
  28. for(int i = 0; i < visibleItemCount; i++){
  29. //获取item
  30. View temp_view = getChildAt(i);
  31. //计算item的中点Y坐标
  32. float itemY = temp_view.getBottom()-(temp_view.getHeight()/2);
  33. //计算离中点的距离
  34. float distance = centerY;
  35. if(itemY > centerY){
  36. distance = itemY - centerY;
  37. }else{
  38. distance = centerY - itemY;
  39. }
  40. //根据距离进行缩放
  41. temp_view.setScaleY(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
  42. temp_view.setScaleX(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
  43. //根据距离改变透明度
  44. temp_view.setAlpha(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
  45. }
  46. }
  47. }
复制代码

这样基本就实现了我们想要的效果了

但是现在有一个问题,就是第一个item和最后一个item无法滚动到中间从而放大突出显示。对此我暂时想了两个方法去解决:1、在头尾各种添加一些空白的item,使我们需要显示的数据都可以滚动到最中间。(我现在就是这么做的);2、使整个列表实现循环滚动。

添加空白的item的话,我是通过修改adapter去实现的。数据源是一个数组,我在数组前面和后面各加一些特殊的数据,adapter实现getview的时候,如果发现当前item的数据是特殊数据,那么item就变透明,从而实现了我们原本要显示的数据都可以被滚动最中间;

先从数据源下手,从头尾填充特殊的数据

  1. public class MainActivity extends AppCompatActivity {
  2. XuListView mLisetview;
  3. MyAdapter adapter;
  4. ArrayList<String> nos = new ArrayList<String>();
  5. @Override
  6. protected void onCreate(Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. setContentView(R.layout.activity_main);
  9. mLisetview = (XuListView) findViewById(R.id.list_test);
  10. ArrayList<String> temp = new ArrayList<String>();
  11. for(int i = 0;i<10;i++){
  12. temp.add(i+"");
  13. }
  14. adapter = new MyAdapter(this,temp);
  15. mLisetview.setAdapter(adapter);
  16. resetitem(mLisetview);
  17. }
  18. /**
  19. * 在头尾填充透明的item数据
  20. */
  21. private void resetitem(ListView listview) {
  22. if(listview == null){
  23. return;
  24. }
  25. //获取屏幕高度
  26. WindowManager wm =getWindowManager();
  27. int displayheight = wm.getDefaultDisplay().getHeight();
  28. //计算一个item的高度
  29. int itemhight = 0;
  30. if(adapter!=null){
  31. View v=(View)adapter.getView(0, null, listview);
  32. v.measure(
  33. View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
  34. View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
  35. itemhight=v.getMeasuredHeight();
  36. }
  37. //根据Item的高度和屏幕的高度计算需要多少个item可以填满一半的屏幕
  38. int newcount = ((displayheight/2)/itemhight);
  39. //填充前面的空白item
  40. for (int i = 1; i <= newcount; i++) {
  41. nos.add("full");
  42. }
  43. //添加我们需要显示的数据
  44. for(int i = 0;i<10;i++){
  45. nos.add(i+"");
  46. }
  47. //填充后面的空白item
  48. for (int i = 1; i <= newcount; i++) {
  49. nos.add("full");
  50. }
  51. //刷新数据
  52. adapter.refreshData(nos);
  53. }
  54. }
复制代码

然后adapter里面对头尾的特殊数据进行识别,将item变为透明的。

  1. public class MyAdapter extends BaseAdapter {
  2. ArrayList<String> nos = new ArrayList<String>();
  3. private Context context;
  4. public MyAdapter(Context context, ArrayList<String> nos){
  5. this.context = context;
  6. this.nos = nos;
  7. }
  8. public void refreshData(ArrayList<String> nos) {
  9. this.nos = nos;
  10. notifyDataSetChanged();
  11. }
  12. @Override
  13. public int getCount() {
  14. return nos.size();
  15. }
  16. @Override
  17. public Object getItem(int position) {
  18. return nos.get(position);
  19. }
  20. @Override
  21. public long getItemId(int position) {
  22. return position;
  23. }
  24. @Override
  25. public View getView(int position, View convertView, ViewGroup parent) {
  26. ViewHolder holder = null;
  27. if (convertView == null) {
  28. // 如果是第一次显示该页面(要记得保存到viewholder中供下次直接从缓存中调用)
  29. holder = new ViewHolder();
  30. convertView = LayoutInflater.from(context).inflate(R.layout.item_test, null);
  31. holder.tv_no = (TextView) convertView.findViewById(R.id.tv_no);
  32. convertView.setTag(holder);
  33. } else {
  34. holder = (ViewHolder) convertView.getTag();
  35. }
  36. holder.tv_no.setText(nos.get(position));
  37. if(nos.get(position).equals("full")){
  38. convertView.setVisibility(View.INVISIBLE);
  39. }else{
  40. convertView.setVisibility(View.VISIBLE);
  41. }
  42. return convertView;
  43. }
  44. private class ViewHolder {
  45. TextView tv_no;
  46. }
  47. }
复制代码

这样我们就实现可以第一种解决方法

第二种方法,就是让整个ListView实现循环滚动,实现的方式有很多,大家可以自行百度,我这里就通过修改adapter的getCount方法去实现,就是在getCount方法return一个很大的值,getView获取数据的时候要模原本的数组大小,不然数组就越界了。然后ListView滚动到最中间,这样就实现伪循环滚动了

  1. public class MainActivity extends AppCompatActivity {
  2. XuListView mLisetview;
  3. MyAdapter adapter;
  4. ArrayList<String> nos = new ArrayList<String>();
  5. @Override
  6. protected void onCreate(Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. setContentView(R.layout.activity_main);
  9. mLisetview = (XuListView) findViewById(R.id.list_test);
  10. ArrayList<String> temp = new ArrayList<String>();
  11. for(int i = 0;i<10;i++){
  12. temp.add(i+"");
  13. }
  14. adapter = new MyAdapter(this,temp);
  15. mLisetview.setAdapter(adapter);
  16. //滚动到中间
  17. mLisetview.setSelection(adapter.getCount()/2);
  18. }
  19. }
复制代码
  1. /**
  2. * Created by xu on 2017/6/27.
  3. */
  4. public class MyAdapter extends BaseAdapter {
  5. ArrayList<String> nos = new ArrayList<String>();
  6. private Context context;
  7. public MyAdapter(Context context, ArrayList<String> nos){
  8. this.context = context;
  9. this.nos = nos;
  10. }
  11. @Override
  12. public int getCount() {
  13. return Integer.MAX_VALUE;
  14. }
  15. @Override
  16. public Object getItem(int position) {
  17. return nos.get(position);
  18. }
  19. @Override
  20. public long getItemId(int position) {
  21. return position;
  22. }
  23. @Override
  24. public View getView(int position, View convertView, ViewGroup parent) {
  25. ViewHolder holder = null;
  26. if (convertView == null) {
  27. // 如果是第一次显示该页面(要记得保存到viewholder中供下次直接从缓存中调用)
  28. holder = new ViewHolder();
  29. convertView = LayoutInflater.from(context).inflate(R.layout.item_test, null);
  30. holder.tv_no = (TextView) convertView.findViewById(R.id.tv_no);
  31. convertView.setTag(holder);
  32. } else {
  33. holder = (ViewHolder) convertView.getTag();
  34. }
  35. holder.tv_no.setText(nos.get(position%nos.size()));
  36. return convertView;
  37. }
  38. private class ViewHolder {
  39. TextView tv_no;
  40. }
  41. }
复制代码

这样我们就实现了使列表进行循环滚动,从而达到每个item都可以滚动到中间突出显示的效果了

既然我们把动画效果都做出来了,那么也可以直接做成一个滚轮选择器,只需要加多两步:1、把最靠近中间的item滚动到中间;2、把中间的item的序号通过一个接口返回出去。 我就直接贴代码吧,反正也不难。

把最靠近中间的item滚动到中间的实现我是这么做的:首先决定好整个ListView可视的的item数量是多少,必须是奇数,这样才能只有一个item处于正中间,然后根据ListView的高度计算出每个item符合要求的高度,然后更改现有的item的高度,同时对内容进行缩放(不缩放内容单纯高度变小的话布局就乱了)。最后我们利用smoothScrollToPosition方法回正可视item中的第一个item,就实现了将最中间的item回滚到最中间的效果了。因为可视的item我们是根据ListView的高度去计算item的高度的,所有的可视item刚好铺满ListView,所以只要顶部那个回正了,其他的item也会回正。所以我们可以重写一下OnScrollListener的onScrollStateChanged方法,每次滚动结束就执行一次回滚

  1. /**
  2. * 可视的item数
  3. */
  4. private int mVisibleItemCount = -1;
  5. /**
  6. * 没调整之前每个item的高度
  7. */
  8. private float olditemheight = 0;
  9. /**
  10. * 调整过后的每个item的高度
  11. */
  12. private float newitemheight = -1;
  13. /**
  14. * 调整每个可视的item的高度 以及对内容进行缩放
  15. */
  16. public void reSetItemHeight() {
  17. for (int i = 0; i < getChildCount(); i++) {
  18. //获取item
  19. View temp_view = getChildAt(i);
  20. //设置item的高度
  21. ViewGroup.LayoutParams lp = temp_view.getLayoutParams();
  22. lp.height = (int) newitemheight;
  23. temp_view.setLayoutParams(lp);
  24. //缩放内容 我的item的内容用一个LinearLayout包了起来 所以直接缩放LinearLayout
  25. LinearLayout item_ll_value = (LinearLayout) temp_view.findViewById(R.id.item_ll_value);
  26. item_ll_value.setScaleY((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight));
  27. item_ll_value.setScaleX((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight));
  28. }
  29. }
  30. /**
  31. * 计算在给定的可视item数目下 每个item应该设置的高度
  32. * */
  33. private void getNewItemHeight() {
  34. //先把旧的item存起来
  35. olditemheight = getChildAt(0).getHeight();
  36. //计算新的高度
  37. newitemheight = getHeight() / mVisibleItemCount;
  38. if ((getHeight() / mVisibleItemCount) % newitemheight > 0) {
  39. //除不尽的情况下把余数分给各个item,暂时发现分一次余数就够了,如果效果不理想就做个递归多分几次
  40. float remainder = (getHeight() / mVisibleItemCount) % newitemheight;
  41. newitemheight = remainder / mVisibleItemCount;
  42. }
  43. }
  44. @Override
  45. public void onScrollStateChanged(AbsListView view, int scrollState) {
  46. //滚动结束之后开始回滚item
  47. if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE && mVisibleItemCount != -1) {
  48. //使离中间最近的item回滚到中点位置
  49. smoothScrollToPosition(getFirstVisiblePosition());
  50. }
  51. }
复制代码

实现了把最靠近中间的item滚动到中间,那么选择的item就是滚动结束后处于最中间的item。对此我们需要再次重写一下OnScrollListener的onScrollStateChanged方法。每次滚动结束后,取可视item中的第一个item的序号加上我们之前设置的可视item数的一半(舍去小数部分)就是最中间的item的序号了,也是当前选择项的序号。

  1. /**
  2. * 当前选中项发生变化的监听者
  3. */
  4. private onSelectionChangeLisenter selectionChangeLisenter;
  5. /**
  6. * 设置选中项的监听者
  7. */
  8. public void setSelectionChangeLisenter(onSelectionChangeLisenter selectionChangeLisenter) {
  9. this.selectionChangeLisenter = selectionChangeLisenter;
  10. }
  11. @Override
  12. public void onScrollStateChanged(AbsListView view, int scrollState) {
  13. //滚动结束之后开始正常回滚item并记录最中间的item为选中项 (必须设置可视项,ListView才会改为选择器模式)
  14. if( scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE && mVisibleItemCount != -1){
  15. //使离中间最近的item回滚到中点位置
  16. smoothScrollToPosition(getFirstVisiblePosition());
  17. //计算当前选中项的序号
  18. int nowPosition = getFirstVisiblePosition() + mVisibleItemCount/2;
  19. //把当前选中项的序号存起来并通过listener回调出去
  20. if(selectionChangeLisenter != null && nowPosition != curPosition){
  21. curPosition = nowPosition;
  22. selectionChangeLisenter.onSelectionChange(curPosition);
  23. }
  24. }
  25. }
复制代码

此处我是使用了一个接口去,用以实时把最新的数据返回出去

  1. /**
  2. * Created by xu on 2017/3/3.
  3. */
  4. public interface onSelectionChangeLisenter {
  5. void onSelectionChange(int position);
  6. }
复制代码

使用这个滚轮选择器的方法也非常简单,除了跟正常的ListView初始化和绑定adapter之外,只需要额外调用两个方法就行了

  1. //设置ListView的可视item数(必须是奇数)
  2. mLisetview.setVisibleItemCount(3);
复制代码
  1. //设置监听者监听选中项的变化
  2. mLisetview.setSelectionChangeLisenter(new onSelectionChangeLisenter() {
  3. @Override
  4. public void onSelectionChange(final int position) {
  5. mHandler.post(new Runnable() {
  6. @Override
  7. public void run() {
  8. Toast.makeText(MainActivity.this,"选择项发生变化 当前选中序号:"+(temp.get(position)),Toast.LENGTH_SHORT).show();
  9. }
  10. });
  11. }
  12. });
复制代码

这样我们就实现滚轮数字选择器的效果了

现在贴下整个滚轮选择器的完整代码

  1. /**
  2. * 模仿滚轮动画缩放的ListView
  3. * Created by xu on 2017/3/3.
  4. */
  5. public class XuListView extends ListView implements AbsListView.OnScrollListener {
  6. private static final String TAG = "XuListView";
  7. /**
  8. * 中点的Y坐标
  9. */
  10. private float centerY = 0f;
  11. /**
  12. * 可视的item数
  13. */
  14. private int mVisibleItemCount = -1;
  15. /**
  16. * 没调整之前每个item的高度
  17. */
  18. private float olditemheight = 0;
  19. /**
  20. * 调整过后的每个item的高度
  21. */
  22. private float newitemheight = -1;
  23. /**
  24. * 当前选中项发生变化的监听者
  25. */
  26. private onSelectionChangeLisenter selectionChangeLisenter;
  27. /**
  28. * 当前选中项的序号
  29. */
  30. private int curPosition = -1;
  31. public XuListView(Context context, AttributeSet attrs) {
  32. super(context, attrs);
  33. //设置一个滚动监听
  34. setOnScrollListener(this);
  35. }
  36. /**
  37. * 设置选中项的监听者
  38. */
  39. public void setSelectionChangeLisenter(onSelectionChangeLisenter selectionChangeLisenter) {
  40. this.selectionChangeLisenter = selectionChangeLisenter;
  41. }
  42. /**
  43. * 设置ListView的显示item数
  44. * @param count :必须是奇数 如果为-1 则表示只是使用动画效果的普通ListView
  45. */
  46. public boolean setVisibleItemCount(int count){
  47. if(count % 2 == 0){
  48. return false;
  49. }else{
  50. mVisibleItemCount = count;
  51. return true;
  52. }
  53. }
  54. /**
  55. * 在这里第一次调整item高度
  56. */
  57. @Override
  58. public void onWindowFocusChanged(boolean hasWindowFocus) {
  59. super.onWindowFocusChanged(hasWindowFocus);
  60. if(mVisibleItemCount != -1){
  61. getNewItemHeight();
  62. reSetItemHeight();
  63. }
  64. }
  65. /**
  66. * 调整每个可视的item的高度 以及对内容进行缩放
  67. */
  68. public void reSetItemHeight(){
  69. for(int i = 0; i < getChildCount(); i++){
  70. //获取item
  71. View temp_view = getChildAt(i);
  72. //设置item的高度
  73. ViewGroup.LayoutParams lp = temp_view.getLayoutParams();
  74. lp.height = (int)newitemheight;
  75. temp_view.setLayoutParams(lp);
  76. //缩放内容 我的item的内容用一个LinearLayout包了起来 所以直接缩放LinearLayout
  77. LinearLayout item_ll_value = (LinearLayout)temp_view.findViewById(R.id.item_ll_value);
  78. item_ll_value.setScaleY((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight));
  79. item_ll_value.setScaleX((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight));
  80. }
  81. }
  82. /**
  83. * 计算在给定的可视item数目下 每个item应该设置的高度
  84. */
  85. private void getNewItemHeight(){
  86. //先把旧的item存起来
  87. olditemheight = getChildAt(0).getHeight();
  88. //计算新的高度
  89. newitemheight = getHeight()/mVisibleItemCount;
  90. if((getHeight()/mVisibleItemCount) % newitemheight > 0){
  91. //除不尽的情况下把余数分给各个item,暂时发现分一次余数就够了,如果效果不理想就做个递归多分几次
  92. float remainder = (getHeight()/mVisibleItemCount) % newitemheight;
  93. newitemheight = remainder/mVisibleItemCount;
  94. }
  95. }
  96. @Override
  97. public void onScrollStateChanged(AbsListView view, int scrollState) {
  98. //滚动结束之后开始正常回滚item并记录最中间的item为选中项 (必须设置可视项,ListView才会改为选择器模式)
  99. if( scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE && mVisibleItemCount != -1){
  100. //使离中间最近的item回滚到中点位置
  101. smoothScrollToPosition(getFirstVisiblePosition());
  102. //计算当前选中项的序号
  103. int nowPosition = getFirstVisiblePosition() + mVisibleItemCount/2;
  104. //把当前选中项的序号存起来并通过listener回调出去
  105. if(selectionChangeLisenter != null && nowPosition != curPosition){
  106. curPosition = nowPosition;
  107. selectionChangeLisenter.onSelectionChange(curPosition);
  108. }
  109. }
  110. }
  111. @Override
  112. public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
  113. //计算中点
  114. centerY = getHeight()/2;
  115. //判断中点的有效性
  116. if(centerY <= 0){
  117. return;
  118. }
  119. //开始对当前显示的View进行缩放
  120. for(int i = 0; i < visibleItemCount; i++){
  121. //获取item
  122. View temp_view = getChildAt(i);
  123. //计算item的中点Y坐标
  124. float itemY = temp_view.getBottom()-(temp_view.getHeight()/2);
  125. //计算离中点的距离
  126. float distance = centerY;
  127. if(itemY > centerY){
  128. distance = itemY - centerY;
  129. }else{
  130. distance = centerY - itemY;
  131. }
  132. //根据距离进行缩放
  133. temp_view.setScaleY(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
  134. temp_view.setScaleX(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
  135. //根据距离改变透明度
  136. temp_view.setAlpha(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
  137. }
  138. }
  139. }
复制代码

注释很详细 相信小白看了也没什么难度。

滚轮效果的实现方式有很多,解决头尾两个item无法滚动到中间的方法也很多,我说的方法仅供参考,没有最好的方法,只有最符合自己的需求的方法。

demo下载地址

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持程序员之家。



回复

使用道具 举报

关闭

站长推荐上一条 /1 下一条