关键计算地方:
1.当前是上滑动还是下滑动(相对于屏幕) ,使用ev.getRawY()获得当前滑动位置在屏幕哪个地方
2. 计算文本客滑动到哪里即可停止, (行高*总文本行数)- (行高 * 最多显示行数) int sum = getLineHeight() * getLineCount() - getLineHeight() * getMaxLines();
代码:
import android.content.Context; import android.text.method.ScrollingMovementMethod; import android.util.AttributeSet; import android.view.MotionEvent; /** * @Description: 可滑动的TextView, 并且解决了与 ScrollView等的滑动冲突 */ public class ScrollTextView extends android.support.v7.widget.AppCompatTextView { public ScrollTextView(Context context) { super(context); setMovementMethod(ScrollingMovementMethod.getInstance()); } public ScrollTextView(Context context, AttributeSet attrs) { super(context, attrs); setMovementMethod(ScrollingMovementMethod.getInstance()); } public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setMovementMethod(ScrollingMovementMethod.getInstance()); } float lastScrollY = 0; @Override public boolean onTouchEvent(MotionEvent ev) { if (getLineCount() > getMaxLines()) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { lastScrollY = ev.getRawY(); } else if (ev.getAction() == MotionEvent.ACTION_MOVE) { //滑动到头并且还在继续上滑动,或者滑动到底部就不要再拦截了(有误差) int sum = getLineHeight() * getLineCount() - getLineHeight() * getMaxLines(); //计算上次与本次差 float diff = lastScrollY - ev.getRawY(); if (diff>0){//下滑动并且到达了底部也不要处理了 //底部这里用abs的原因是,因为计算sum的时候有些误差 if (Math.abs(sum - getScrollY())<5) { getParent().requestDisallowInterceptTouchEvent(false); } else { getParent().requestDisallowInterceptTouchEvent(true); } }else if (diff<0){//上滑动 if (getScrollY() == 0) {//上滑动并且已经到达了顶部就不要在处理了 getParent().requestDisallowInterceptTouchEvent(false); } else { getParent().requestDisallowInterceptTouchEvent(true); } } lastScrollY = ev.getRawY(); } else { getParent().requestDisallowInterceptTouchEvent(false); } } return super.onTouchEvent(ev); } }
如果上面方法不能解决你的问题,那就参考下面的文章,让textview实现 NestedScroolChild3 接口,并重写相应的方法,从而实现TextView嵌套滑动。
WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
我使用的代码:
package com.*.view; import android.annotation.SuppressLint; import android.text.StaticLayout; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.text.Layout; import android.text.TextPaint; import android.text.method.ScrollingMovementMethod; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewParent; import android.widget.OverScroller; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.NestedScrollingChild3; import androidx.core.view.NestedScrollingChildHelper; import androidx.core.view.ViewCompat; public class JustifyTextView extends androidx.appcompat.widget.AppCompatTextView implements NestedScrollingChild3 { private static final String TAG = "JustifyTextView"; private int mLineY = 0;//总行高 private int mViewWidth;//TextView的总宽度 private TextPaint paint; private static final int INVALID_POINTER = -1; private final int[] mScrollOffset = new int[2]; private final int[] mScrollConsumed = new int[2]; private int mLastMotionY; private NestedScrollingChildHelper mChildHelper; private boolean mIsBeingDragged = false; private VelocityTracker mVelocityTracker; private int mTouchSlop; private int mActivePointerId = INVALID_POINTER; private int mNestedYOffset; private OverScroller mScroller; private int mMinimumVelocity; private int mMaximumVelocity; private int mLastScrollerY; public JustifyTextView(Context context) { super(context); init(); } public JustifyTextView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public JustifyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mScroller = new OverScroller(getContext()); mChildHelper = new NestedScrollingChildHelper(this); } @Override protected void onDraw(Canvas canvas) { paint = getPaint(); paint.setColor(getCurrentTextColor()); paint.drawableState = getDrawableState(); mLineY = 0; mViewWidth = getMeasuredWidth();//获取textview的实际宽度 mLineY += getTextSize(); String text = getText().toString(); Layout layout = getLayout(); int lineCount = layout.getLineCount(); for (int i = 0; i < lineCount; i++) {//每行循环 int lineStart = layout.getLineStart(i); int lineEnd = layout.getLineEnd(i); String lineText = text.substring(lineStart, lineEnd);//获取TextView每行中的内容 if (needScale(lineText)) { if (i == lineCount - 1) {//最后一行不需要重绘 canvas.drawText(lineText, 0, mLineY, paint); } else { float width = StaticLayout.getDesiredWidth(text, lineStart, lineEnd, paint); drawScaleText(canvas, lineText, width); } } else { canvas.drawText(lineText, 0, mLineY, paint); } mLineY += getLineHeight();//写完一行以后,高度增加一行的高度 } } /** * 重绘此行. * * @param canvas 画布 * @param lineText 该行所有的文字 * @param lineWidth 该行每个文字的宽度的总和 */ private void drawScaleText(Canvas canvas, String lineText, float lineWidth) { float x = 0; if (isFirstLineOfParagraph(lineText)) { String blanks = " "; canvas.drawText(blanks, x, mLineY, paint); float width = StaticLayout.getDesiredWidth(blanks, paint); x += width; lineText = lineText.substring(3); } //比如说一共有5个字,中间有4个间隔, //那就用整个TextView的宽度 - 5个字的宽度, //然后除以4,填补到这4个空隙中 float interval = (mViewWidth - lineWidth) / (lineText.length() - 1); for (int i = 0; i < lineText.length(); i++) { String character = String.valueOf(lineText.charAt(i)); float cw = StaticLayout.getDesiredWidth(character, paint); canvas.drawText(character, x, mLineY, paint); x += (cw + interval); } } /** * 判断是不是段落的第一行. * 一个汉字相当于一个字符,此处判断是否为第一行的依据是: * 字符长度大于3且前两个字符为空格 * * @param lineText 该行所有的文字 */ private boolean isFirstLineOfParagraph(String lineText) { return lineText.length() > 3 && lineText.charAt(0) == ' ' && lineText.charAt(1) == ' '; } /** * 判断需不需要缩放. * * @param lineText 该行所有的文字 * @return true 该行最后一个字符不是换行符 false 该行最后一个字符是换行符 */ private boolean needScale(String lineText) { if (lineText.isEmpty()) { return false; } else { return lineText.charAt(lineText.length() - 1) != '\n'; } } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent ev) { initVelocityTrackerIfNotExists(); MotionEvent vtev = MotionEvent.obtain(ev); final int actionMasked = ev.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } vtev.offsetLocation(0, mNestedYOffset); switch (actionMasked) { case MotionEvent.ACTION_DOWN: if ((mIsBeingDragged = !mScroller.isFinished())) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } if (!mScroller.isFinished()) { abortAnimatedScroll(); } mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); break; case MotionEvent.ACTION_MOVE: final int activePointerIndex = ev.findPointerIndex(mActivePointerId); if (activePointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); break; } final int y = (int) ev.getY(activePointerIndex); int deltaY = mLastMotionY - y; if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) { deltaY -= mScrollConsumed[1]; mNestedYOffset += mScrollOffset[1]; } if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } mIsBeingDragged = true; if (deltaY > 0) { deltaY -= mTouchSlop; } else { deltaY += mTouchSlop; } } if (mIsBeingDragged) { mLastMotionY = y - mScrollOffset[1]; final int oldY = getScrollY(); final int range = getScrollRange(); // Calling overScrollByCompat will call onOverScrolled, which // calls onScrollChanged if applicable. if (overScrollByCompat(0, deltaY, 0, oldY, 0, range, 0, 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) { mVelocityTracker.clear(); } final int scrolledDeltaY = getScrollY() - oldY; final int unconsumedY = deltaY - scrolledDeltaY; mScrollConsumed[1] = 0; dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, ViewCompat.TYPE_TOUCH, mScrollConsumed); mLastMotionY -= mScrollOffset[1]; mNestedYOffset += mScrollOffset[1]; } break; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); if ((Math.abs(initialVelocity) > mMinimumVelocity)) { if (!dispatchNestedPreFling(0, -initialVelocity)) { dispatchNestedFling(0, -initialVelocity, true); fling(-initialVelocity); } } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } mActivePointerId = INVALID_POINTER; endDrag(); break; case MotionEvent.ACTION_CANCEL: if (mIsBeingDragged) { if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } } mActivePointerId = INVALID_POINTER; endDrag(); break; case MotionEvent.ACTION_POINTER_DOWN: final int index = ev.getActionIndex(); mLastMotionY = (int) ev.getY(index); mActivePointerId = ev.getPointerId(index); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); break; } if (mVelocityTracker != null) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return super.onTouchEvent(ev); } private void abortAnimatedScroll() { mScroller.abortAnimation(); stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); } private void endDrag() { mIsBeingDragged = false; recycleVelocityTracker(); stopNestedScroll(); } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; final int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mLastMotionY = (int) ev.getY(newPointerIndex); mActivePointerId = ev.getPointerId(newPointerIndex); if (mVelocityTracker != null) { mVelocityTracker.clear(); } } } private void fling(int velocityY) { int height = getHeight(); mScroller.fling(getScrollX(), getScrollY(), // start 0, velocityY, // velocities 0, 0, // x Integer.MIN_VALUE, Integer.MAX_VALUE, // y 0, height / 2); runAnimatedScroll(true); } private void runAnimatedScroll(boolean participateInNestedScrolling) { if (participateInNestedScrolling) { startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH); } else { stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); } mLastScrollerY = getScrollY(); ViewCompat.postInvalidateOnAnimation(this); } private void initOrResetVelocityTracker() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } else { mVelocityTracker.clear(); } } private void initVelocityTrackerIfNotExists() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } } private void recycleVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { // this is causing double scroll call (doubled speed), but this WebView isn't overscrollable // all overscrolls are passed to appbar, so commenting this out during drag if (!mIsBeingDragged) overScrollByCompat(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); // without this call webview won't scroll to top when url change or when user pick input // (webview should move a bit making input still in viewport when "adjustResize") return true; } int getScrollRange() { //Using scroll range of webview instead of childs as NestedScrollView does. return computeVerticalScrollRange(); } @Override public boolean isNestedScrollingEnabled() { return mChildHelper.isNestedScrollingEnabled(); } @Override public void setNestedScrollingEnabled(boolean enabled) { mChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean startNestedScroll(int axes, int type) { return mChildHelper.startNestedScroll(axes, type); } @Override public boolean startNestedScroll(int axes) { return startNestedScroll(axes, ViewCompat.TYPE_TOUCH); } @Override public void stopNestedScroll(int type) { mChildHelper.stopNestedScroll(type); } @Override public void stopNestedScroll() { stopNestedScroll(ViewCompat.TYPE_TOUCH); } @Override public boolean hasNestedScrollingParent(int type) { return mChildHelper.hasNestedScrollingParent(type); } @Override public boolean hasNestedScrollingParent() { return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, ViewCompat.TYPE_TOUCH); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type) { return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type); } @Override public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) { mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mChildHelper.dispatchNestedFling(velocityX, velocityY, false); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); } @Override public void computeScroll() { if (mScroller.isFinished()) { return; } mScroller.computeScrollOffset(); final int y = mScroller.getCurrY(); int unconsumed = y - mLastScrollerY; mLastScrollerY = y; // Nested Scrolling Pre Pass mScrollConsumed[1] = 0; dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH); unconsumed -= mScrollConsumed[1]; if (unconsumed != 0) { // Internal Scroll final int oldScrollY = getScrollY(); overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, getScrollRange(), 0, 0, false); final int scrolledByMe = getScrollY() - oldScrollY; unconsumed -= scrolledByMe; // Nested Scrolling Post Pass mScrollConsumed[1] = 0; dispatchNestedScroll(0, 0, 0, unconsumed, mScrollOffset, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed); unconsumed -= mScrollConsumed[1]; } if (unconsumed != 0) { abortAnimatedScroll(); } if (!mScroller.isFinished()) { ViewCompat.postInvalidateOnAnimation(this); } } // copied from NestedScrollView exacly as it looks, leaving overscroll related code, maybe future use private boolean overScrollByCompat(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { final int overScrollMode = getOverScrollMode(); final boolean canScrollHorizontal = computeHorizontalScrollRange() > computeHorizontalScrollExtent(); final boolean canScrollVertical = computeVerticalScrollRange() > computeVerticalScrollExtent(); final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); int newScrollX = scrollX + deltaX; if (!overScrollHorizontal) { maxOverScrollX = 0; } int newScrollY = scrollY + deltaY; if (!overScrollVertical) { maxOverScrollY = 0; } // Clamp values if at the limits and record final int left = -maxOverScrollX; final int right = maxOverScrollX + scrollRangeX; final int top = -maxOverScrollY; final int bottom = maxOverScrollY + scrollRangeY; boolean clampedX = false; if (newScrollX > right) { newScrollX = right; clampedX = true; } else if (newScrollX < left) { newScrollX = left; clampedX = true; } boolean clampedY = false; if (newScrollY > bottom) { newScrollY = bottom; clampedY = true; } else if (newScrollY < top) { newScrollY = top; clampedY = true; } if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange()); } onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); return clampedX || clampedY; } }
分享:
定时垂直滚动的textview:GitHub - paradoxie/AutoVerticalTextview: 垂直滚动的textview,继承自TextSwitcher,抽出一个依赖库供以后备用
左右对齐的TextView
GitHub - Giftedcat/JustifyTextView: 左右对齐的TextView,适配各种分辨率,完美实现UI需求
(解决了最后一行显示不全的问题,但是颜色显示有问题)
修改也很简单,就是把paint.setColor方法移到onDraw里面,代码如下:
import android.content.Context; import android.graphics.Canvas; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.widget.TextView; /** * Android自定义TextView实现文字两端能够对齐的TextView,文字右端能够对齐的TextView * http://blog.csdn.net/kikitious_du/article/details/50561927 */ public class JustifyTextView extends TextView { private int mLineY = 0;//总行高 private int mViewWidth;//TextView的总宽度 private TextPaint paint; public JustifyTextView(Context context) { super(context); init(); } public JustifyTextView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public JustifyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { paint = getPaint(); } @Override protected void onDraw(Canvas canvas) { paint.setColor(getCurrentTextColor()); paint.drawableState = getDrawableState(); mLineY = 0; mViewWidth = getMeasuredWidth();//获取textview的实际宽度 mLineY += getTextSize(); String text = getText().toString(); Layout layout = getLayout(); int lineCount = layout.getLineCount(); for (int i = 0; i < lineCount; i++) {//每行循环 int lineStart = layout.getLineStart(i); int lineEnd = layout.getLineEnd(i); String lineText = text.substring(lineStart, lineEnd);//获取TextView每行中的内容 if (needScale(lineText)) { if (i == lineCount - 1) {//最后一行不需要重绘 canvas.drawText(lineText, 0, mLineY, paint); } else { float width = StaticLayout.getDesiredWidth(text, lineStart, lineEnd, paint); drawScaleText(canvas, lineText, width); } } else { canvas.drawText(lineText, 0, mLineY, paint); } mLineY += getLineHeight();//写完一行以后,高度增加一行的高度 } } /** * 重绘此行. * * @param canvas 画布 * @param lineText 该行所有的文字 * @param lineWidth 该行每个文字的宽度的总和 */ private void drawScaleText(Canvas canvas, String lineText, float lineWidth) { float x = 0; if (isFirstLineOfParagraph(lineText)) { String blanks = " "; canvas.drawText(blanks, x, mLineY, paint); float width = StaticLayout.getDesiredWidth(blanks, paint); x += width; lineText = lineText.substring(3); } //比如说一共有5个字,中间有4个间隔, //那就用整个TextView的宽度 - 5个字的宽度, //然后除以4,填补到这4个空隙中 float interval = (mViewWidth - lineWidth) / (lineText.length() - 1); for (int i = 0; i < lineText.length(); i++) { String character = String.valueOf(lineText.charAt(i)); float cw = StaticLayout.getDesiredWidth(character, paint); canvas.drawText(character, x, mLineY, paint); x += (cw + interval); } } /** * 判断是不是段落的第一行. * 一个汉字相当于一个字符,此处判断是否为第一行的依据是: * 字符长度大于3且前两个字符为空格 * * @param lineText 该行所有的文字 */ private boolean isFirstLineOfParagraph(String lineText) { return lineText.length() > 3 && lineText.charAt(0) == ' ' && lineText.charAt(1) == ' '; } /** * 判断需不需要缩放. * * @param lineText 该行所有的文字 * @return true 该行最后一个字符不是换行符 false 该行最后一个字符是换行符 */ private boolean needScale(String lineText) { if (lineText.length() == 0) { return false; } else { return lineText.charAt(lineText.length() - 1) != '\n'; } } }