Android自定義View繪圖實(shí)現(xiàn)拖影動(dòng)畫(huà)
前幾天在“Android繪圖之漸隱動(dòng)畫(huà)”一文中通過(guò)畫(huà)線實(shí)現(xiàn)了漸隱動(dòng)畫(huà),但里面有個(gè)問(wèn)題,畫(huà)筆較粗(大于1)時(shí)線段之間會(huì)有裂隙,我又改進(jìn)了一下。這次效果好多了。
先看效果吧:

然后我們來(lái)說(shuō)說(shuō)基本的做法:
•根據(jù)畫(huà)筆寬度,計(jì)算每一條線段兩個(gè)頂點(diǎn)對(duì)應(yīng)的四個(gè)點(diǎn),四點(diǎn)連線,包圍線段,形成一個(gè)路徑。
•后一條線段的路徑的前兩個(gè)點(diǎn),?。ǖ扔冢┣耙粭l線段的后兩點(diǎn),這樣就銜接起來(lái)了。
把Path的Style修改為FILL,效果是這樣的:

可以看到一個(gè)個(gè)四邊形,連成了路徑。
好啦,現(xiàn)在說(shuō)說(shuō)怎樣根據(jù)兩點(diǎn)計(jì)算出包圍它們連線的路徑所需的四個(gè)點(diǎn)。
先看一張圖:

在這張圖里,黑色細(xì)線是我們拿到的兩個(gè)觸摸點(diǎn)相連得到的。當(dāng)畫(huà)筆寬度大于1(比如為10)時(shí),其實(shí)經(jīng)過(guò)這條黑線的兩個(gè)端點(diǎn)并且與這條黑線垂直相交的兩條線(藍(lán)線),就可以計(jì)算出來(lái),藍(lán)線的長(zhǎng)度就是畫(huà)筆的寬度,結(jié)合這些就可以計(jì)算出紅色的四個(gè)點(diǎn)。而紅色的四個(gè)點(diǎn)就圍住了線段,形成路徑。
這里面用到兩點(diǎn)連線的公式,采用點(diǎn)斜式:
y = k*x + b
黑線的斜率是:
k = (y2 - y1) / (x2 - x1)
垂直相交的兩條線的斜率的關(guān)系是:
k1 * k2 = -1
所以,藍(lán)線的斜率就可以計(jì)算出來(lái)了。有了斜率和線上的一個(gè)點(diǎn),就可以求出這條線的點(diǎn)斜式中的b,點(diǎn)斜式就出來(lái)了。
然后,利用兩點(diǎn)間距離公式:

已知一個(gè)點(diǎn),這個(gè)點(diǎn)與另一個(gè)點(diǎn)的距離(畫(huà)筆寬度除以2),斜率,代入兩點(diǎn)間距離公式和藍(lán)線的點(diǎn)斜式,就可以計(jì)算出兩個(gè)紅色的點(diǎn)了。
計(jì)算時(shí)用到的是一元二次方程a*x*x + bx + c = 0,求 x 時(shí)用的公式是:

好啦,最后,上代碼:
package com.example.disappearinglines;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
/**
* Created by foruok,歡迎關(guān)注我的訂閱號(hào)“程序視界”.
*/
public class DisappearingDoodleView extends View {
public static float convertDipToPx(Context context, float fDip) {
float fPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, fDip,
context.getResources().getDisplayMetrics());
return fPx;
}
final static String TAG = "DoodleView";
class LineElement {
static final public int ALPHA_STEP = 8;
public LineElement(float pathWidth){
mPaint = new Paint();
mPaint.setARGB(255, 255, 0, 0);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(0);
mPaint.setStrokeCap(Paint.Cap.BUTT);
mPaint.setStyle(Paint.Style.FILL);
mPath = new Path();
mPathWidth = pathWidth;
for(int i= 0; i < mPoints.length; i++){
mPoints[i] = new PointF();
}
}
public void setPaint(Paint paint){
mPaint = paint;
}
public void setAlpha(int alpha){
mPaint.setAlpha(alpha);
mPathWidth = (alpha * mPathWidth) / 255;
}
private boolean caculatePoints(float k, float b, float x1, float y1, float distance, PointF pt1, PointF pt2){
//point-k formula
// y= kx + b
//distance formula of two points
// distance*distance = Math.pow((x - x1), 2) + Math.pow((y - y1), 2)
// |
// V
// ax*x + bx + c = 0;
// |
// V
// x = (-b +/- Math.sqrt( b*b - 4*a*c ) ) / (2*a)
double a1 = Math.pow(k, 2) + 1;
double b1 = 2* k * (b - y1) - 2 * x1;
double c1 = Math.pow(x1, 2) + Math.pow(b - y1, 2) - Math.pow(distance, 2);
double criterion = Math.pow(b1, 2) - 4*a1*c1;
if(criterion > 0) {
criterion = Math.sqrt(criterion);
pt1.x = (float) ((-b1 + criterion) / (2 * a1));
pt1.y = k * pt1.x + b;
pt2.x = (float) ((-b1 - criterion) / (2 * a1));
pt2.y = k * pt2.x + b;
return true;
}
return false;
}
private void swapPoint(PointF pt1, PointF pt2){
float t = pt1.x;
pt1.x = pt2.x;
pt2.x = t;
t = pt1.y;
pt1.y = pt2.y;
pt2.y = t;
}
public boolean updatePathPoints(){
float distance = mPathWidth / 2;
if(Math.abs(mEndX - mStartX) < 1){
mPoints[0].x = mStartX + distance;
mPoints[0].y = mStartY - distance;
mPoints[1].x = mStartX - distance;
mPoints[1].y = mPoints[0].y;
mPoints[2].x = mPoints[1].x;
mPoints[2].y = mEndY + distance;
mPoints[3].x = mPoints[0].x;
mPoints[3].y = mPoints[2].y;
}else if(Math.abs(mEndY - mStartY) < 1){
mPoints[0].x = mStartX - distance;
mPoints[0].y = mStartY - distance;
mPoints[1].x = mPoints[0].x;
mPoints[1].y = mStartY + distance;
mPoints[2].x = mEndX + distance;
mPoints[2].y = mPoints[1].y;
mPoints[3].x = mPoints[2].x;
mPoints[3].y = mPoints[0].y;
}else{
//point-k formula
//y= kx + b
float kLine = (mEndY - mStartY) / (mEndX - mStartX);
float kVertLine = -1 / kLine;
float b = mStartY - (kVertLine * mStartX);
if(!caculatePoints(kVertLine, b, mStartX, mStartY, distance, mPoints[0], mPoints[1])){
String info = String.format(TAG, "startPt, criterion < 0, (%.2f, %.2f)-->(%.2f, %.2f), kLine - %.2f, kVertLine - %.2f, b - %.2f",
mStartX, mStartY, mEndX, mEndY, kLine, kVertLine, b);
Log.i(TAG, info);
return false;
}
b = mEndY - (kVertLine * mEndX);
if(!caculatePoints(kVertLine, b, mEndX, mEndY, distance, mPoints[2], mPoints[3])){
String info = String.format(TAG, "endPt, criterion < 0, (%.2f, %.2f)-->(%.2f, %.2f), kLine - %.2f, kVertLine - %.2f, b - %.2f",
mStartX, mStartY, mEndX, mEndY, kLine, kVertLine, b);
Log.i(TAG, info);
return false;
}
//reorder points to unti-clockwise
if(mStartX < mEndX){
if(mStartY < mEndY){
if(mPoints[0].x < mPoints[1].x){
swapPoint(mPoints[0], mPoints[1]);
}
if(mPoints[2].x > mPoints[3].x){
swapPoint(mPoints[2], mPoints[3]);
}
}else{
if(mPoints[0].x > mPoints[1].x){
swapPoint(mPoints[0], mPoints[1]);
}
if(mPoints[2].x < mPoints[3].x){
swapPoint(mPoints[2], mPoints[3]);
}
}
}else{
if(mStartY < mEndY){
if(mPoints[0].x < mPoints[1].x){
swapPoint(mPoints[0], mPoints[1]);
}
if(mPoints[2].x > mPoints[3].x){
swapPoint(mPoints[2], mPoints[3]);
}
}else{
if(mPoints[0].x > mPoints[1].x){
swapPoint(mPoints[0], mPoints[1]);
}
if(mPoints[2].x < mPoints[3].x){
swapPoint(mPoints[2], mPoints[3]);
}
}
}
}
return true;
}
// for the first line
public void updatePath(){
//update path
mPath.reset();
mPath.moveTo(mPoints[0].x, mPoints[0].y);
mPath.lineTo(mPoints[1].x, mPoints[1].y);
mPath.lineTo(mPoints[2].x, mPoints[2].y);
mPath.lineTo(mPoints[3].x, mPoints[3].y);
mPath.close();
}
// for middle line
public void updatePathWithStartPoints(PointF pt1, PointF pt2){
mPath.reset();
mPath.moveTo(pt1.x, pt1.y);
mPath.lineTo(pt2.x, pt2.y);
mPath.lineTo(mPoints[2].x, mPoints[2].y);
mPath.lineTo(mPoints[3].x, mPoints[3].y);
mPath.close();
}
public float mStartX = -1;
public float mStartY = -1;
public float mEndX = -1;
public float mEndY = -1;
public Paint mPaint;
public Path mPath;
public PointF[] mPoints = new PointF[4]; //path's vertex
float mPathWidth;
}
private LineElement mCurrentLine = null;
private List<LineElement> mLines = null;
private float mLaserX = 0;
private float mLaserY = 0;
final Paint mPaint = new Paint();
private int mWidth = 0;
private int mHeight = 0;
private long mElapsed = 0;
private float mStrokeWidth = 20;
private float mCircleRadius = 10;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg){
DisappearingDoodleView.this.invalidate();
}
};
public DisappearingDoodleView(Context context){
super(context);
initialize(context);
}
public DisappearingDoodleView(Context context, AttributeSet attrs){
super(context, attrs);
initialize(context);
}
private void initialize(Context context){
mStrokeWidth = convertDipToPx(context, 22);
mCircleRadius = convertDipToPx(context, 10);
mPaint.setARGB(255, 255, 0, 0);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(0);
mPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void onSizeChanged (int w, int h, int oldw, int oldh){
mWidth = w;
mHeight = h;
adjustLasterPosition();
}
private void adjustLasterPosition(){
if(mLaserX - mCircleRadius < 0) mLaserX = mCircleRadius;
else if(mLaserX + mCircleRadius > mWidth) mLaserX = mWidth - mCircleRadius;
if(mLaserY - mCircleRadius < 0) mLaserY = mCircleRadius;
else if(mLaserY + mCircleRadius > mHeight) mLaserY = mHeight - mCircleRadius;
}
private void updateLaserPosition(float x, float y){
mLaserX = x;
mLaserY = y;
adjustLasterPosition();
}
@Override
protected void onDraw(Canvas canvas){
//canvas.drawText("ABCDE", 10, 16, mPaint);
mElapsed = SystemClock.elapsedRealtime();
if(mLines != null) {
updatePaths();
for (LineElement e : mLines) {
if(e.mStartX < 0 || e.mEndY < 0 || e.mPath.isEmpty()) continue;
//canvas.drawLine(e.mStartX, e.mStartY, e.mEndX, e.mEndY, e.mPaint);
canvas.drawPath(e.mPath, e.mPaint);
}
compactPaths();
}
canvas.drawCircle(mLaserX, mLaserY, mCircleRadius, mPaint);
}
private boolean isValidLine(float x1, float y1, float x2, float y2){
return Math.abs(x1 - x2) > 1 || Math.abs(y1 - y2) > 1;
}
@Override
public boolean onTouchEvent(MotionEvent event){
float x = event.getX();
float y = event.getY();
int action = event.getAction();
if(action == MotionEvent.ACTION_UP){// end one line after finger release
if(isValidLine(mCurrentLine.mStartX, mCurrentLine.mStartY, x, y)){
mCurrentLine.mEndX = x;
mCurrentLine.mEndY = y;
addToPaths(mCurrentLine);
}
//mCurrentLine.updatePathPoints();
mCurrentLine = null;
updateLaserPosition(x, y);
invalidate();
return true;
}
if(action == MotionEvent.ACTION_DOWN){
mLines = null;
mCurrentLine = new LineElement(mStrokeWidth);
mCurrentLine.mStartX = x;
mCurrentLine.mStartY = y;
updateLaserPosition(x, y);
return true;
}
if(action == MotionEvent.ACTION_MOVE) {
if(isValidLine(mCurrentLine.mStartX, mCurrentLine.mStartY, x, y)){
mCurrentLine.mEndX = x;
mCurrentLine.mEndY = y;
addToPaths(mCurrentLine);
mCurrentLine = new LineElement(mStrokeWidth);
mCurrentLine.mStartX = x;
mCurrentLine.mStartY = y;
updateLaserPosition(x, y);
}else{
//do nothing, wait next point
}
}
if(mHandler.hasMessages(1)){
mHandler.removeMessages(1);
}
Message msg = new Message();
msg.what = 1;
mHandler.sendMessageDelayed(msg, 0);
return true;
}
private void addToPaths(LineElement element){
if(mLines == null) {
mLines = new ArrayList<LineElement>() ;
}
mLines.add(element);
}
private void updatePaths() {
int size = mLines.size();
if (size == 0) return;
LineElement line = null;
int j = 0;
for (; j < size; j++) {
line = mLines.get(j);
if (line.updatePathPoints()) break;
}
if (j == size) {
mLines.clear();
return;
} else {
for (j--; j >= 0; j--) {
mLines.remove(0);
}
}
line.updatePath();
size = mLines.size();
LineElement lastLine = null;
for (int i = 1; i < size; i++) {
line = mLines.get(i);
if (line.updatePathPoints()){
if (lastLine == null) {
lastLine = mLines.get(i - 1);
}
line.updatePathWithStartPoints(lastLine.mPoints[3], lastLine.mPoints[2]);
lastLine = null;
}else{
mLines.remove(i);
size = mLines.size();
}
}
}
public void compactPaths(){
int size = mLines.size();
int index = size - 1;
if(size == 0) return;
int baseAlpha = 255 - LineElement.ALPHA_STEP;
int itselfAlpha;
LineElement line;
for(; index >=0 ; index--, baseAlpha -= LineElement.ALPHA_STEP){
line = mLines.get(index);
itselfAlpha = line.mPaint.getAlpha();
if(itselfAlpha == 255){
if(baseAlpha <= 0 || line.mPathWidth < 1){
++index;
break;
}
line.setAlpha(baseAlpha);
}else{
itselfAlpha -= LineElement.ALPHA_STEP;
if(itselfAlpha <= 0 || line.mPathWidth < 1){
++index;
break;
}
line.setAlpha(itselfAlpha);
}
}
if(index >= size){
// all sub-path should disappear
mLines = null;
}
else if(index >= 0){
//Log.i(TAG, "compactPaths from " + index + " to " + (size - 1));
mLines = mLines.subList(index, size);
}else{
// no sub-path should disappear
}
long interval = 40 - SystemClock.elapsedRealtime() + mElapsed;
if(interval < 0) interval = 0;
Message msg = new Message();
msg.what = 1;
mHandler.sendMessageDelayed(msg, interval);
}
}
這樣自繪,效率不太好,還沒(méi)想怎么去改進(jìn),大家可以討論討論。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
android動(dòng)態(tài)設(shè)置app當(dāng)前運(yùn)行語(yǔ)言的方法
下面小編就為大家?guī)?lái)一篇android動(dòng)態(tài)設(shè)置app當(dāng)前運(yùn)行語(yǔ)言的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-03-03
Android中RecyclerView實(shí)現(xiàn)簡(jiǎn)單購(gòu)物車(chē)功能
這篇文章主要為大家詳細(xì)介紹了Android中RecyclerView實(shí)現(xiàn)簡(jiǎn)單購(gòu)物車(chē)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02
Android實(shí)現(xiàn)沉浸式狀態(tài)欄功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)沉浸式狀態(tài)欄功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10
Android實(shí)現(xiàn)GridView中的item自由拖動(dòng)效果
在前一個(gè)項(xiàng)目中,實(shí)現(xiàn)了一個(gè)功能是gridview中的item自由拖到效果,實(shí)現(xiàn)思路很簡(jiǎn)單,主要工作就是交換節(jié)點(diǎn),以及拖動(dòng)時(shí)的移動(dòng)效果,下面小編給大家分享具體實(shí)現(xiàn)過(guò)程,對(duì)gridview實(shí)現(xiàn)拖拽效果感興趣的朋友一起看看吧2016-11-11
android開(kāi)發(fā)實(shí)現(xiàn)文件讀寫(xiě)
這篇文章主要為大家詳細(xì)介紹了android開(kāi)發(fā)實(shí)現(xiàn)文件讀寫(xiě),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07
Android開(kāi)發(fā)之ListView的簡(jiǎn)單用法及定制ListView界面操作示例
這篇文章主要介紹了Android開(kāi)發(fā)之ListView的簡(jiǎn)單用法及定制ListView界面操作,結(jié)合實(shí)例形式分析了Android ListView界面布局相關(guān)操作技巧,需要的朋友可以參考下2019-04-04
Kotlin Flow常用封裝類(lèi)StateFlow使用詳解
這篇文章主要為大家介紹了Kotlin Flow常用封裝類(lèi)StateFlow使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
android webview 簡(jiǎn)單瀏覽器實(shí)現(xiàn)代碼
android webview 簡(jiǎn)單瀏覽器實(shí)現(xiàn)代碼,需要的朋友可以參考一下2013-05-05
Android實(shí)現(xiàn)H5與Native交互的兩種方式
Android實(shí)現(xiàn)H5頁(yè)面和Native頁(yè)面交互的方法有兩種,一種是Url攔截的方法,另一種是JavaScript注入,下面來(lái)通過(guò)這篇文章分別講解。有需要的朋友們可以參考借鑒,下面來(lái)一起看看吧。2016-12-12
利用Android畫(huà)圓弧canvas.drawArc()實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于利用Android畫(huà)圓弧canvas.drawArc()的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的理解和學(xué)習(xí)具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11

