android 循环自定义View

本文介绍如何在Android中创建一个自定义ViewGroup,实现固定数量的子View循环滑动的效果。详细讨论了布局设计以及使用FOCUS_BLOCK_DESCENDANTS参数来控制焦点转移的行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

效果:固定的子view个数,循环滑动,第一个子View向左滑即滑到最后一个子View,最后一个子View向后右滑即滑到第一个子View

效果图如下:


布局:可以看出LoopViewGroup实现循环滑动,包括七张ImageView 图片

<RelativeLayout 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" >

    <Button
        android:id="@+id/btn_remove"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="remove" />

    <com.example.loop.LoopViewGroup
        android:id="@+id/loop_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="50dp" >

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="fitXY"
            android:src="@drawable/image1"
            android:tag="image1" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="fitXY"
            android:src="@drawable/image2"
            android:tag="image2" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="fitXY"
            android:src="@drawable/image3"
            android:tag="image3" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="fitXY"
            android:src="@drawable/image4"
            android:tag="image4" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="fitXY"
            android:src="@drawable/image5"
            android:tag="image5" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="fitXY"
            android:src="@drawable/image6"
            android:tag="image6" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="fitXY"
            android:src="@drawable/image7"
            android:tag="image7" />
    </com.example.loop.LoopViewGroup>

</RelativeLayout>

首先说一说ViewGroup中方法setDescendantFocusability的作用

有三个参数FOCUS_BEFORE_DESCENDANTS,FOCUS_AFTER_DESCENDANTS,FOCUS_BLOCK_DESCENDANTS,此例中会使用FOCUS_BLOCK_DESCENDANTS这个参数。先看ViewGroup源码中的调用:

/**
     * {@inheritDoc}
     *
     * Looks for a view to give focus to respecting the setting specified by
     * {@link #getDescendantFocusability()}.
     *
     * Uses {@link #onRequestFocusInDescendants(int, android.graphics.Rect)} to
     * find focus within the children of this group when appropriate.
     *
     * @see #FOCUS_BEFORE_DESCENDANTS
     * @see #FOCUS_AFTER_DESCENDANTS
     * @see #FOCUS_BLOCK_DESCENDANTS
     * @see #onRequestFocusInDescendants(int, android.graphics.Rect) 
     */
    @Override
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " ViewGroup.requestFocus direction="
                    + direction);
        }
        int descendantFocusability = getDescendantFocusability();

        switch (descendantFocusability) {
            case FOCUS_BLOCK_DESCENDANTS:
                return super.requestFocus(direction, previouslyFocusedRect);
            case FOCUS_BEFORE_DESCENDANTS: {
                final boolean took = super.requestFocus(direction, previouslyFocusedRect);
                return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
            }
            case FOCUS_AFTER_DESCENDANTS: {
                final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
                return took ? took : super.requestFocus(direction, previouslyFocusedRect);
            }
            default:
                throw new IllegalStateException("descendant focusability must be "
                        + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
                        + "but is " + descendantFocusability);
        }
    }

FOCUS_BLOCK_DESCENDANTS参数不会传递到子View,FOCUS_BEFORE_DESCENDANTS先判断当前是否能获取焦点,否则就传递给子View,FOCUS_AFTER_DESCENDANTS则相反,先判断子View是否可以获取焦点。


LoopViewGroup的代码:

package com.example.loop;

import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.OnHierarchyChangeListener;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.Scroller;

public class LoopViewGroup extends ViewGroup implements View.OnFocusChangeListener, OnHierarchyChangeListener {
	public static final int DIRECTION_BACKWARD = 0;
	public static final int DIRECTION_FORWARD = 1;

	private int mDirection = DIRECTION_FORWARD;
	private List<View> mImages = new ArrayList<View>();
	private int mCurIndex = 0;
	private int mPreIndex = 0;

	protected Scroller mScroller;

	private int mItemWidth;
	private int mItemheight;

	private OnSelectedListener mListener;

	public LoopViewGroup(Context context) {
		this(context, null);
	}

	public LoopViewGroup(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	//
	public LoopViewGroup(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		setChildrenDrawingOrderEnabled(true);
		setFocusable(true);
		setFocusableInTouchMode(true);
		setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
		// view的添加和删除监听
		setOnHierarchyChangeListener(this);
		// 允许在dispatchDraw和onDraw中画
		setWillNotDraw(false);
		setOnFocusChangeListener(this);

		final Resources res = getResources();
		mItemWidth = res.getDimensionPixelSize(R.dimen.item_width);
		mItemheight = res.getDimensionPixelSize(R.dimen.item_height);

		mScroller = new Scroller(getContext(), new DecelerateInterpolator());
		scrollTo(0, 0);
	}

	public void setOnSelectedListener(OnSelectedListener l) {
		mListener = l;
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

		int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

		int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
		int count = getChildCount();
		int newWidth = getPaddingLeft() + getPaddingRight();
		int newHeight = getPaddingTop() + getPaddingBottom() + mItemheight;
		for (int i = 0; i < count; i++) {
			View child = getChildAt(i);
			child.measure(MeasureSpec.makeMeasureSpec(mItemWidth, MeasureSpec.EXACTLY),
					MeasureSpec.makeMeasureSpec(mItemheight, MeasureSpec.EXACTLY));
			measureChild(child, widthMeasureSpec, heightMeasureSpec);
			newWidth += mItemWidth;
		}
		setMeasuredDimension(Math.min(newWidth, widthSpecSize), Math.min(newHeight, heightSpecSize));
	}

	// 根据方向以及当前的选中和先前的选中layout子view
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		int count = mImages.size();
		int scrollX = getScrollX();
		int paddingLeft = getPaddingLeft();
		int paddingTop = getPaddingTop();
		int left = scrollX + paddingLeft;
		int right = 0;
		int top = paddingTop;
		int bottom = 0;
		int startIndex = 0;
		if (mCurIndex == mPreIndex) {
			startIndex = mCurIndex;
		} else if (mDirection == DIRECTION_BACKWARD) {
			left += -(mItemWidth);
			startIndex = mCurIndex;
		} else {
			startIndex = mCurIndex - 1 < 0 ? count - 1 : mCurIndex - 1;
		}
		for (int i = startIndex; i < count + startIndex - 1; i++) {
			int index = i;
			if (i >= count) {
				index = i - count;
			}
			View child = mImages.get(index);
			right = left + mItemWidth;
			top = paddingTop;
			bottom = top + mItemheight;
			child.layout(left, top, right, bottom);
			left += mItemWidth;
		}
	}

	// 设置选中的index
	public void setIndex(int index, int direction) {
		int size = mImages.size();
		if (index >= 0 && index < size) {
			mPreIndex = mCurIndex;
			mCurIndex = index;
			// 调用选中监听
			if (mListener != null) {
				mListener.onSelectedChange(mImages.get(mCurIndex), mCurIndex);
			}
			if (mCurIndex == mPreIndex) {
				return;
			} else {
				// 开始滑动动画
				mDirection = direction;
				if (!mScroller.isFinished()) {
					mScroller.abortAnimation();
					scrollTo(mScroller.getFinalX(), mScroller.getFinalY());
				}
				requestLayout();
				int scrollX = getScrollX();
				int dx;
				if (mDirection == DIRECTION_BACKWARD) {
					dx = -mItemWidth;
				} else {
					dx = mItemWidth;
				}
				mScroller.startScroll(scrollX, 0, dx, 0, 400);
				invalidate();
			}
		} else {
			if (mListener != null) {
				mListener.onNothingSelected();
			}
		}
	}

	// 获取选中的view
	public View getSelectedView() {
		int count = mImages.size();
		if (mCurIndex >= 0 && mCurIndex < count) {
			return mImages.get(mCurIndex);
		}
		return null;
	}

	// 画出焦点框
	@Override
	protected void dispatchDraw(Canvas canvas) {
		super.dispatchDraw(canvas);

		if (isFocused()) {
			Drawable d = getResources().getDrawable(R.drawable.item_border);
			Rect padding = new Rect();
			d.getPadding(padding);
			int left = getPaddingLeft() + getScrollX() - padding.left;
			int top = getPaddingTop() + getScrollY() - padding.top;
			int right = left + mItemWidth + padding.left + padding.right;
			int bottom = top + mItemheight + padding.top + padding.bottom;
			d.setBounds(left, top, right, bottom);
			d.draw(canvas);
		}
	}

	// 左右移动时判断哪一个view选中,return true
	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
			int size = mImages.size();
			final int curIndex = mCurIndex;
			if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
				if (curIndex > 0) {
					setIndex(curIndex - 1, DIRECTION_BACKWARD);
				} else {
					setIndex(size - 1, DIRECTION_BACKWARD);
				}
			} else {
				if (curIndex < size - 1) {
					setIndex(curIndex + 1, DIRECTION_FORWARD);
				} else {
					setIndex(0, DIRECTION_FORWARD);
				}
			}
			return true;
		}
		return super.onKeyDown(keyCode, event);
	}

	// 当前view的焦点变化,重绘出焦点框
	@Override
	public void onFocusChange(View view, boolean hasFocus) {
		setIndex(mCurIndex, DIRECTION_FORWARD);
		invalidate();
	}

	// 子view添加
	@Override
	public void onChildViewAdded(View parent, View child) {
		mImages.add(child);
	}

	// 子view移除
	@Override
	public void onChildViewRemoved(View parent, View child) {
		int index = mImages.indexOf(child);
		if (index >= 0) {
			mImages.remove(child);
			setIndex(Math.min(mCurIndex, mImages.size() - 1), DIRECTION_FORWARD);
		}
	}

	// 判断滑动动画是否完成
	@Override
	public void computeScroll() {
		super.computeScroll();
		if (mScroller.computeScrollOffset()) {
			if (getScrollX() != mScroller.getCurrX() || getScrollY() != mScroller.getCurrY()) {
				scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
			}
			invalidate();
		} else if (mScroller.isFinished()) {

		}
	}

	@Override
	protected void onDetachedFromWindow() {
		super.onDetachedFromWindow();
		mImages.clear();
		mListener = null;
	}

	static class ScrollInterpolator implements Interpolator {
		public ScrollInterpolator() {
		}

		public float getInterpolation(float t) {
			t -= 1.0f;
			return t * t * t * t * t + 1;
		}
	}

	// 监听view的选中
	public interface OnSelectedListener {
		public void onSelectedChange(View v, int index);

		public void onNothingSelected();
	}

}

在MainActivity中的使用:

package com.example.loop;

import com.example.loop.LoopViewGroup.OnSelectedListener;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.app.Activity;

public class MainActivity extends Activity implements OnClickListener, OnSelectedListener {

	public static final String TAG = "Loop MainActivity";

	LoopViewGroup mLoopViewGroup;

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

		mLoopViewGroup = (LoopViewGroup) findViewById(R.id.loop_view);
		mLoopViewGroup.setOnSelectedListener(this);
		//findViewById(R.id.btn_remove).setOnClickListener(this);
	}

	@Override
	public void onSelectedChange(View v, int index) {
		Log.d(TAG, "onSelectedChange tag : " + v.getTag().toString() + "  index = " + index);
	}

	@Override
	public void onNothingSelected() {
		Log.d(TAG, "onNothingSelected");
	}

	@Override
	public void onClick(View v) {

		if (v == null) {
			return;
		}

		int id = v.getId();
		switch (id) {
		case R.id.btn_remove:
			// Log.d(TAG, "btn remove click");
			// View selectedView = mLoopViewGroup.getSelectedView();
			// if (selectedView != null) {
			// Log.d(TAG, "remove view : " + selectedView.getTag().toString());
			// mLoopViewGroup.removeView(selectedView);
			// }
			break;

		default:
			break;
		}

	}

}

代码链接


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值