android实现TextView垂直或水平滚动

下面将介绍TextView实现滚动的三种方式:

1、嵌套在ScrollView或者HorizontalScrollView中

垂直滚动:
<Scrollview android:layout_width="fill_parent"
    android:layout_height="fill_parent" android:scrollbars="vertical">
    <Textview android:text="http://orgcent.com ..."/>
</Scrollview>

水平滚动:使用标签<Horizontalscrollview>

2、设置ScrollingMovementMethod
代码中添加:

TextView.setMovementMethod(new ScrollingMovementMethod());

XML中配置:

android:scrollbars="vertical"

3、使用Scroller来自定义TextView
点击查看:android自定义View-垂直滚动的TextView

测试DEMO: 点击查看

android自定义View-垂直滚动的TextView

其实要让TextView能够滚动,可以使用ScrollView/HorizontalScrollView或者设置ScrollingMovementMethod来实现。
点击查看:android实现TextView垂直或水平滚动

下面自定义垂直滚动的TextView,主要是用来学习Scroller的使用。关于ScrollTextView的实现,可以看下面的介绍和源码。

package com.orgcent.demo.view;

import android.content.Context;
import android.util.AttributeSet;
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;

    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();
	//设置滚动减速器,在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();
		final int maxY = (getLineCount() * getLineHeight() + getPaddingTop() + getPaddingBottom()) - getHeight();
		boolean toEdge = scrollY < 0 || scrollY > maxY;
		if(scrollY < 0)
			scrollY = 0;
		else if(scrollY > maxY)
			scrollY = maxY;

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

    public void fling(int velocityY) {
	final int maxY = (getLineCount() * getLineHeight() + getPaddingTop() + getPaddingBottom()) - getHeight();

        mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
                Math.max(0, maxY));

        //刷新,让父控件调用computeScroll()
        invalidate();
    }

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

	return handled | super.onTouchEvent(ev);
    }

    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();
            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);
		int deltaY = (int) (mLastMotionY - y);

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

			/*
			 * 默认滚动时间为250ms,建议立即滚动,否则滚动效果不明显
			 * 或者直接使用scrollBy(0, deltaY);
			 */
			mScroller.startScroll(getScrollX(), getScrollY(), 0, deltaY, 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);

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

		mActivePointerId = INVALID_POINTER;
                mIsBeingDragged = false;

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

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

测试DEMO: 点击查看

Android根据分辨率进行单位转换-(dp,sp转像素px)

Android系统中,默认的单位是像素(px)。也就是说,在没有明确说明的情况下,所有的大小设置都是以像素为单位。

如果以像素设置大小,会导致不同分辨率下出现不同的效果。那么,如何将应用中所有大小的单位都设置为’dp’呢?
实际上TextView.setTextSize()重载了根据单位设置大小的方法。

笔者在此基础上实现了以下方法:

/**
 * 获取当前分辨率下指定单位对应的像素大小(根据设备信息)
 * px,dip,sp -> px
 *
 * Paint.setTextSize()单位为px
 *
 * 代码摘自:TextView.setTextSize()
 *
 * @param unit  TypedValue.COMPLEX_UNIT_*
 * @param size
 * @return
 */
public float getRawSize(int unit, float size) {
       Context c = getContext();
       Resources r;

       if (c == null)
           r = Resources.getSystem();
       else
           r = c.getResources();

       return TypedValue.applyDimension(unit, size, r.getDisplayMetrics());
}

继续阅读

Android自定义view-文本自动换行

文本自动换行原理:文本超出控件宽度后,自动换到下一行绘制。

实现代码:

protected void onDraw(Canvas canvas) {
        FontMetrics fm = mPaint.getFontMetrics();

	float baseline = fm.descent - fm.ascent;
	float x = 0;
	float y =  baseline;  //由于系统基于字体的底部来绘制文本,所有需要加上字体的高度。

	String txt = getResources().getString(com.orgcent.demo.R.string.hello);

	//文本自动换行
	String[] texts = autoSplit(txt, mPaint, getWidth() - 5);

	System.out.printf("line indexs: %s\n", Arrays.toString(texts));

	for(String text : texts) {
		canvas.drawText(text, x, y, mPaint);  //坐标以控件左上角为原点
		y += baseline + fm.leading; //添加字体行间距
	}
}

继续阅读

android自定义view–绘制顺序及相关原理

Android系统中要自定义view,首先需要了解Android的view加载机制。主要有三个方法:

1、onMeasure()     //计算出view自身大小
2、onLayout()     //仅在ViewGroup中,用来为子view指定位置(left,top)
3、onDraw()      //view绘制内容

那么系统能让我们在onDraw()能够绘制些什么呢,查看View.draw()源码发现: 点击查看源码
view的draw()方法

下面根据源码中的相关说明,进一步分析控件的绘制操作及顺序:

/*
 * Draw traversal performs several drawing steps which must be executed
 * in the appropriate order:
 *
 *  1. Draw the background  (绘制控件设置的背景,系统已在view.draw()中绘制,只要在xml中指定背景即可)
 *  2. If necessary, save the canvas' layers to prepare for fading
 *  3. Draw view's content  (可以重写, onDraw(canvas);)
 *  4. Draw children      (可重写,用来分发canvas到子控件,具体看ViewGroup。对应方法dispatchDraw(canvas);此方法依次调用了子控件的draw()方法)
 *  5. If necessary, draw the fading edges and restore layers (绘制控件四周的阴影渐变效果)
 *  6. Draw decorations (scrollbars for instance) (用来绘制滚动条,对应方法onDrawScrollBars(canvas);。
 *      onDrawHorizontalScrollBar()和onDrawVerticalScrollBar()被隐藏了无法重写,也许有其他方法重写滚动条)
 */