Android PathMeasure工具类笔记(仿UC loaddingview)

加载视图绘制
本文介绍了一个自定义加载视图的实现方法,通过PathMeasure类截取路径的方式展示加载进度,同时实现了视图中眼睛和嘴巴的动态效果。

前言:午休难得有时间闲下来看看新闻,打开uc,打开了一张美女图片,然后看到这么一个画面
这里写图片描述

这个loaddingview深深吸引着我,此时我很不愿意让这张图片加载出来了(loaddingview的魅力已经大于美女的魅力了),尼玛!!这就是程序猿的世界么??哈哈~~~

好啦!不扯了,进入正题,今天我们要实现的就是这么一个loaddingview,先来一张最终的效果:
这里写图片描述

显示图片:

这里写图片描述

实现方式:
1、用贝塞尔曲线画一个四角是round的正方形,然后按进度画出进度条path。
2、用pathmeasure类根据当前的进度截取path,然后画出path。

小伙伴也不要思维定式了,也有很多种实现方式(关键是有想法一定要实践,嘻嘻~)今天我们主要讲pathmeasure类这种方式。

先介绍下PathMeasure这个类(顾名思义,PathMeasure是一个用来测量Path的类)主要有以下方法::

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.graphics;

public class PathMeasure {
    private Path mPath;

    /**
     * Create an empty PathMeasure object. To uses this to measure the length
     * of a path, and/or to find the position and tangent along it, call
     * setPath.
     *
     * Note that once a path is associated with the measure object, it is
     * undefined if the path is subsequently modified and the the measure object
     * is used. If the path is modified, you must call setPath with the path.
     */
    public PathMeasure() {
        mPath = null;
        native_instance = native_create(0, false);
    }

    /**
     * Create a PathMeasure object associated with the specified path object
     * (already created and specified). The measure object can now return the
     * path's length, and the position and tangent of any position along the
     * path.
     *
     * Note that once a path is associated with the measure object, it is
     * undefined if the path is subsequently modified and the the measure object
     * is used. If the path is modified, you must call setPath with the path.
     *
     * @param path The path that will be measured by this object
     * @param forceClosed If true, then the path will be considered as "closed"
     *        even if its contour was not explicitly closed.
     */
    public PathMeasure(Path path, boolean forceClosed) {
        // The native implementation does not copy the path, prevent it from being GC'd
        mPath = path;
        native_instance = native_create(path != null ? path.ni() : 0,
                                        forceClosed);
    }

    /**
     * Assign a new path, or null to have none.
     */
    public void setPath(Path path, boolean forceClosed) {
        mPath = path;
        native_setPath(native_instance,
                       path != null ? path.ni() : 0,
                       forceClosed);
    }

    /**
     * Return the total length of the current contour, or 0 if no path is
     * associated with this measure object.
     */
    public float getLength() {
        return native_getLength(native_instance);
    }

    /**
     * Pins distance to 0 <= distance <= getLength(), and then computes the
     * corresponding position and tangent. Returns false if there is no path,
     * or a zero-length path was specified, in which case position and tangent
     * are unchanged.
     *
     * @param distance The distance along the current contour to sample
     * @param pos If not null, eturns the sampled position (x==[0], y==[1])
     * @param tan If not null, returns the sampled tangent (x==[0], y==[1])
     * @return false if there was no path associated with this measure object
    */
    public boolean getPosTan(float distance, float pos[], float tan[]) {
        if (pos != null && pos.length < 2 ||
            tan != null && tan.length < 2) {
            throw new ArrayIndexOutOfBoundsException();
        }
        return native_getPosTan(native_instance, distance, pos, tan);
    }

    public static final int POSITION_MATRIX_FLAG = 0x01;    // must match flags in SkPathMeasure.h
    public static final int TANGENT_MATRIX_FLAG  = 0x02;    // must match flags in SkPathMeasure.h

    /**
     * Pins distance to 0 <= distance <= getLength(), and then computes the
     * corresponding matrix. Returns false if there is no path, or a zero-length
     * path was specified, in which case matrix is unchanged.
     *
     * @param distance The distance along the associated path
     * @param matrix Allocated by the caller, this is set to the transformation
     *        associated with the position and tangent at the specified distance
     * @param flags Specified what aspects should be returned in the matrix.
     */
    public boolean getMatrix(float distance, Matrix matrix, int flags) {
        return native_getMatrix(native_instance, distance, matrix.native_instance, flags);
    }

    /**
     * Given a start and stop distance, return in dst the intervening
     * segment(s). If the segment is zero-length, return false, else return
     * true. startD and stopD are pinned to legal values (0..getLength()).
     * If startD <= stopD then return false (and leave dst untouched).
     * Begin the segment with a moveTo if startWithMoveTo is true.
     *
     * <p>On {@link android.os.Build.VERSION_CODES#KITKAT} and earlier
     * releases, the resulting path may not display on a hardware-accelerated
     * Canvas. A simple workaround is to add a single operation to this path,
     * such as <code>dst.rLineTo(0, 0)</code>.</p>
     */
    public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) {
        dst.isSimplePath = false;
        return native_getSegment(native_instance, startD, stopD, dst.ni(), startWithMoveTo);
    }

    /**
     * Return true if the current contour is closed()
     */
    public boolean isClosed() {
        return native_isClosed(native_instance);
    }

    /**
     * Move to the next contour in the path. Return true if one exists, or
     * false if we're done with the path.
     */
    public boolean nextContour() {
        return native_nextContour(native_instance);
    }

    protected void finalize() throws Throwable {
        native_destroy(native_instance);
    }

    private static native long native_create(long native_path, boolean forceClosed);
    private static native void native_setPath(long native_instance, long native_path, boolean forceClosed);
    private static native float native_getLength(long native_instance);
    private static native boolean native_getPosTan(long native_instance, float distance, float pos[], float tan[]);
    private static native boolean native_getMatrix(long native_instance, float distance, long native_matrix, int flags);
    private static native boolean native_getSegment(long native_instance, float startD, float stopD, long native_path, boolean startWithMoveTo);
    private static native boolean native_isClosed(long native_instance);
    private static native boolean native_nextContour(long native_instance);
    private static native void native_destroy(long native_instance);

    /* package */private final long native_instance;
}

实现原理都是一些native的方法,主要java方法:

方法名说明
PathMeasure ()构造方法
PathMeasure(Path path, boolean forceClosed)创建 PathMeasure 并关联一个指定的Path(Path需要已经创建完成)。
setPath(Path path, boolean forceClosed)path:关联传入的path
isClosed()是否闭合
getLength()获取path的长度
nextContour()跳转到下一个轮廓
getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)截取相应的path
getPosTan(float distance, float[] pos, float[] tan)根据传入的distance获取当前path的位置跟tan值
getMatrix(float distance, Matrix matrix, int flags)获取指定长度的位置坐标及该点Matrix

方法使用起来都很简单,我们这次的demo中需要用到getSegment根据当前进度截取path,然后显示出来(没错!就是这么简单)

 public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) {
        dst.isSimplePath = false;
        return native_getSegment(native_instance, startD, stopD, dst.ni(), startWithMoveTo);
    }

我们需要截取一段path需要传入参数:截取的起始点(startD)、截取的结束点(stopD)、目标path,startWithMoveTo起始点是否使用 moveTo 用于保证截取的 Path 第一个点位置不变。

代码比较简单,都有注释,我就不一一解释了:

package com.leo.camerroll.camera;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.RectF;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;

/**
 * Created by leo on 17/2/10.
 */

public class LoaddingView extends View {
    /**
     * 左眼距离左边的距离(控件宽度*EYE_PERCENT_W),
     * 右眼距离右边的距离(控件宽度*EYE_PERCENT_W)
     */
    private static final float EYE_PERCENT_W = 0.35F;
    /**
     *眼睛距离top的距离(控件的高度*EYE_PERCENT_H)
     */
    private static final float EYE_PERCENT_H = 0.38F;
    /**
     * 嘴巴左边跟右边距离top的距离(控件的高度*MOUCH_PERCENT_H)
     */
    private static final float MOUCH_PERCENT_H = 0.55F;
    /**
     * 嘴巴中间距离top的距离(控件的高度*MOUCH_PERCENT_H2)
     */
    private static final float MOUCH_PERCENT_H2 = 0.7F;
    /**
     * 嘴巴左边跟右边距离边缘的位置(控件宽度*MOUCH_PERCENT_W)
     */
    private static final float MOUCH_PERCENT_W = 0.23F;
    /**
     * 眼睛跟嘴巴摆动的区间范围
     */
    private static final float DURATION_AREA = 0.15F;
    /**
     * 眼睛跟嘴巴摆动的动画
     */
    Animation a=new Animation() {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            float offset=interpolatedTime*DURATION_AREA;
            mMouchH=MOUCH_PERCENT_H+offset;
            mMouchH2=MOUCH_PERCENT_H2+offset;
            mEyesH=EYE_PERCENT_H+offset;
            postInvalidate();
        }
    };
    private Paint reachedPaint;
    private Paint unreachedPaint;
    private Path reachedPath;
    private Path unreachedPath;
    private Path mouthPath=new Path();

    private float mProgress=0.1f;
    private float lineWidth=dp2px(2);

    private float mRadius;
    private float mMouchH=MOUCH_PERCENT_H;
    private float mMouchH2=MOUCH_PERCENT_H2;
    private float mEyesH=EYE_PERCENT_H;
    public LoaddingView(Context context) {
        this(context, null);
    }

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

    public LoaddingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void startAni() {
        a.setDuration(500);
        a.setRepeatCount(Animation.INFINITE);
        a.setRepeatMode(Animation.REVERSE);
        startAnimation(a);
    }

    private void initView() {
        reachedPaint=new Paint(Paint.ANTI_ALIAS_FLAG| Paint.DITHER_FLAG);
        reachedPaint.setStyle(Paint.Style.STROKE);
        reachedPaint.setStrokeWidth(lineWidth);
        reachedPaint.setColor(Color.WHITE);
        reachedPaint.setStrokeJoin(Paint.Join.ROUND);
        reachedPaint.setStrokeCap(Paint.Cap.ROUND);


        unreachedPaint=new Paint(reachedPaint);
        unreachedPaint.setColor(Color.GRAY);
    }
    private boolean isStart=true;
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if(isStart){
            startAni();
            isStart=false;
        }
        mRadius=getWidth()/7F/2;
        if(unreachedPath==null){
            unreachedPath=new Path();
        }
        unreachedPath.addRoundRect(new RectF(lineWidth,lineWidth,w-lineWidth,h-lineWidth),w/6,w/6, Path.Direction.CCW);
        if(reachedPath==null){
            reachedPath=new Path();
        }
        reachedPath.addRoundRect(new RectF(lineWidth,lineWidth,w-lineWidth,h-lineWidth),w/6,w/6, Path.Direction.CW);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.TRANSPARENT);
        canvas.save();
        //draw face
        drawFace(canvas);
        //drawreached rect
        drawReachedRect(canvas);
        canvas.restore();
    }

    /**
     * draw face
     */
    private void drawFace(Canvas canvas) {
        unreachedPaint.setStyle(Paint.Style.FILL);
        //画左边的眼睛
        canvas.drawCircle(getWidth()*EYE_PERCENT_W,getHeight()*mEyesH-mRadius,mRadius,unreachedPaint);
        //画右边的眼睛
        canvas.drawCircle(getWidth()*(1-EYE_PERCENT_W),getHeight()*mEyesH-mRadius,mRadius,unreachedPaint);
        mouthPath.reset();
        //画嘴巴
        mouthPath.moveTo(getWidth()*MOUCH_PERCENT_W,getHeight()*mMouchH);
        mouthPath.quadTo(getWidth()/2,getHeight()*mMouchH2,getWidth()*(1-MOUCH_PERCENT_W),getHeight()*mMouchH);
        unreachedPaint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(mouthPath,unreachedPaint);
    }

    private void drawReachedRect(Canvas canvas) {
        unreachedPaint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(unreachedPath,unreachedPaint);
        PathMeasure measure=new PathMeasure(reachedPath,false);
        float length = measure.getLength();
        //获取当前path长度
        float currLength=length*mProgress;
        Path path=new Path();
        /**
         * 因为uc的起始位置是在顶部的位置,而我们的path的起始位置是在左下的位置,
         * 所以我们需要加上一个length*1/3f偏移量
         */
        measure.getSegment(length*1/3f,currLength+length*1/3f,path,true);
        canvas.drawPath(path,reachedPaint);
        /**
         * 当mProgress>=2/3f的时候,也就是回到了起点的时候,我们得截取两段path了
         * 一段是1/3的位置到2/3
         * 一段是0到1/3的位置
         */
        if(mProgress>=2/3f){
            Path path2=new Path();
            measure.getSegment(0,length*(mProgress-2/3f),path2,true);
            canvas.drawPath(path2,reachedPaint);
        }

    }

    public float dp2px(float dpValue){
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dpValue,getResources().getDisplayMetrics());
    }
    public void setProgress(float progress){
        Log.e("TAG",""+progress);
        if(progress<mProgress){
            return;
        }
        this.mProgress=progress;
        if(Looper.myLooper()==Looper.getMainLooper()){
            invalidate();
        }else{
            postInvalidate();
        }
    }

    public void loadComplete() {
        a.cancel();
        clearAnimation();
        setVisibility(View.GONE);
    }
}

最后附上github链接:
https://github.com/913453448/CamerRoll

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值