水平滚动的textview

本文介绍了一个自定义的ScrollTextView组件,该组件实现了基于触摸的手动滚动及惯性滚动功能。文章详细解释了如何通过计算滚动偏移量实现文本的平滑滚动,并提供了完整的代码示例。

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

package com.example.mmmmm;

import android.R.integer;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.view.animation.DecelerateInterpolator;
import android.widget.Scroller;
import android.widget.TextView;

/**
 * 配置android:scrollbars="vertical",启用垂直滚动条
 * 
 * 实现水平滚动就是修改mScrollX,可参考HorizontalScrollView
 * 
 * 滚动原理: 在绘制前对画布进行偏移操作
 * 
 * 下面是View的绘制机制: |- view.computeScroll()
 * --用来对mScrollX/Y进行修改。由于在绘制前调用,可调用invalite()来触发 |-
 * canvas.translate(-mScrollX,-mScrollY) --偏移画布 |- view.draw() --绘制
 * 
 * 上述内容可以在View.buildDrawingCache()或ViewGroup.dispatchDraw()->drawChild()中找到.
 * 直接查看方法名即可
 * 
 * 滚动帮助类: Scroller --用来计算滚动后的偏移值.具体请参考ScrollView和HorizontalScrollView
 * VelocityTracker --速度计算类。根据fling时的按下、抬起动作,计算滚动初速度
 * 
 * ScrollTextView--流程解析: 1、onTouchEvent() --使用Scroller来计算滚动偏移值
 * 2、重写computeScroll() --对View的mScrollY进行修改, 此处控制滚动范围
 * 
 * 滚动范围: 最小值:0 最大值:所有文本高度+内边距-View高度。也就是超出屏幕的文本高度
 */
public class ScrollTextView extends TextView {
	private Scroller mScroller;
	private int mTouchSlop;
	private int mMinimumVelocity;
	private int mMaximumVelocity;

	private float mLastMotionY;
	private boolean mIsBeingDragged;
	private VelocityTracker mVelocityTracker;
	private int mActivePointerId = INVALID_POINTER;

	private static final int INVALID_POINTER = -1;
	final Paint mTextPaint = new Paint();

	/**
	 * calculate the scrolling length of the text in pixel
	 * 
	 * @return the scrolling length in pixels
	 */
	private int calculateScrollingLen() {
		TextPaint tp = getPaint();
		Rect rect = new Rect();
		String strTxt = getText().toString();
		tp.getTextBounds(strTxt, 0, strTxt.length(), rect);
		int scrollingLen = rect.width() + getWidth();
		rect = null;
		return scrollingLen;
	}
	
	public ScrollTextView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		initView();
	}

	public ScrollTextView(Context context, AttributeSet attrs) {
		super(context, attrs);
		initView();
	}

	public ScrollTextView(Context context) {
		super(context);
		initView();
	}

	private void initView() {
		final Context cx = getContext();
		super.setSingleLine(true);
		// 设置滚动减速器,在fling中会用到
		mScroller = new Scroller(cx, new DecelerateInterpolator(0.5f));
		final ViewConfiguration configuration = ViewConfiguration.get(cx);
		mTouchSlop = configuration.getScaledTouchSlop();
		mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
		mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
		

	}

	/**
	 * 此方法为最后机会来修改mScrollX,mScrollY. 这方法后将根据mScrollX,mScrollY来偏移Canvas已实现内容滚动
	 */
	@Override
	public void computeScroll() {
		super.computeScroll();

		final Scroller scroller = mScroller;
		if (scroller.computeScrollOffset()) { // 正在滚动,让view滚动到当前位置

			int scrollY = scroller.getCurrY();

			int scrollx = scroller.getCurrX();

			final int maxY = (getLineCount() * getLineHeight()
					+ getPaddingTop() + getPaddingBottom())
					- getHeight();
			final int maxX = (int) ((calculateScrollingLen() + getPaddingLeft() + getPaddingRight()) - getWidth());
			Log.e("text.computeScroll", "getWidth="+getWidth()+"\t scrollx="+scrollx+"\t maxX=" + maxX);
			boolean toEdge = scrollY < 0 || scrollY > maxY;
			if (scrollY < 0)
				scrollY = 0;
			else if (scrollY > maxY)
				scrollY = maxY;

			boolean toedg2 = scrollx < 0 || scrollx > maxX;
			if (scrollx < 0)
				scrollx = 0;
			else if (scrollx > maxX-getWidth())
				scrollx = maxX-getWidth();
			/*
			 * 下面等同于: mScrollY = scrollY; awakenScrollBars(); //显示滚动条,必须在xml中配置。
			 * postInvalidate();
			 */
			// scrollTo(0, scrollY);
			scrollTo(scrollx, 0);
			/*
			 * if (toEdge) // 移到两端,由于位置没有发生变化,导致滚动条不显示 awakenScrollBars();
			 */
			if (toedg2) {
				awakenScrollBars();
			}
		}
	}

	public void fling(int velocityX) {
		Log.e("text.fling", "velocityX=" + velocityX);
		final int maxY = (getLineCount() * getLineHeight() + getPaddingTop() + getPaddingBottom())
				- getHeight();

		final int maxX = (int) ((calculateScrollingLen()+ getPaddingLeft() + getPaddingRight()) - getWidth());

		/*
		 * mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
		 * Math.max(0, maxY));
		 */
		Log.e("text.fling", "maxX=" + maxX);
		mScroller.fling(getScrollX(), getScrollY(), velocityX, 0, 0,
				Math.max(0, maxX), 0, 0);
		// 刷新,让父控件调用computeScroll()
		invalidate();
	}

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		/*
		 * 事件处理方式:先自己处理后交给父类处理。 PS:方式不同,可能导致效果不同。请根据需求自行修改。
		 */
		boolean handled = false;

		/*
		 * final int contentHeight = getLineCount() * getLineHeight(); if
		 * (contentHeight > getHeight()) { handled = processScroll(ev); }
		 */

		final int contentwidth = calculateScrollingLen();
		if (contentwidth > getWidth()) {
			handled = processScroll(ev);
		}

		return handled | super.onTouchEvent(ev);
	}

	private float mLasMotionX = 0;

	private boolean processScroll(MotionEvent ev) {
		boolean handled = false;
		if (mVelocityTracker == null) {
			mVelocityTracker = VelocityTracker.obtain();
		}
		mVelocityTracker.addMovement(ev); // 帮助类,用来在fling时计算移动初速度

		final int action = ev.getAction();

		switch (action) {
		case MotionEvent.ACTION_DOWN: {
			if (!mScroller.isFinished()) {
				mScroller.forceFinished(true);
			}

			mLastMotionY = ev.getY();
			mLasMotionX = ev.getX();

			mActivePointerId = ev.getPointerId(0);
			mIsBeingDragged = true;
			handled = true;
			break;
		}
		case MotionEvent.ACTION_MOVE: {
			final int pointerId = mActivePointerId;
			if (mIsBeingDragged && INVALID_POINTER != pointerId) {
				final int pointerIndex = ev.findPointerIndex(pointerId);
				final float y = ev.getY(pointerIndex);
				final float x = ev.getX(pointerIndex);

				int deltaY = (int) (mLastMotionY - y);

				int deltaX = (int) (mLasMotionX - x);

				/*
				 * if (Math.abs(deltaY) > mTouchSlop) { //
				 * 移动距离(正负代表方向)必须大于ViewConfiguration设置的默认值 mLastMotionY = y;
				 * 
				 * /* 默认滚动时间为250ms,建议立即滚动,否则滚动效果不明显 或者直接使用scrollBy(0, deltaY);
				 * 
				 * mScroller.startScroll(getScrollX(), getScrollY(), 0, deltaY,
				 * 0); invalidate(); handled = true; }
				 */

				if (Math.abs(deltaX) > mTouchSlop) { // 移动距离(正负代表方向)必须大于ViewConfiguration设置的默认值
					mLasMotionX = x;

					/*
					 * 默认滚动时间为250ms,建议立即滚动,否则滚动效果不明显 或者直接使用scrollBy(0, deltaY);
					 */
					mScroller.startScroll(getScrollX(), getScrollY(), deltaX,
							0, 0);
					invalidate();
					handled = true;
				}

			}
			break;
		}
		case MotionEvent.ACTION_UP: {
			final int pointerId = mActivePointerId;
			if (mIsBeingDragged && INVALID_POINTER != pointerId) {
				final VelocityTracker velocityTracker = mVelocityTracker;
				velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);

				int initialVelocity = (int) velocityTracker
						.getYVelocity(pointerId);
				int initialVelocitx = (int) velocityTracker
						.getXVelocity(pointerId);

				/*
				 * if (Math.abs(initialVelocity) > mMinimumVelocity) {
				 * fling(-initialVelocity); }
				 */

				if (Math.abs(initialVelocitx) > mMinimumVelocity) {
					fling(-initialVelocitx);
				}

				mActivePointerId = INVALID_POINTER;
				mIsBeingDragged = false;

				if (mVelocityTracker != null) {
					mVelocityTracker.recycle();
					mVelocityTracker = null;
				}

				handled = true;
			}
			break;
		}
		}
		return handled;
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值