自定义View(二):滑动的路线图(HorizontalRoadView)

由于毕业设计需要,做了个滑动的路线图,没有用动态截图工具,只能看截图:


         

中间白色部分 为自定义的视图,可以触摸滑动,同时可以触摸设置当前位置。视图分为三部分,车及标记点的背景图路线、以及地点。下面一步步自定义出我们要的效果。

首先看一下自定义View的步骤:

1、自定义View的属性

2、在View的构造方法中获得我们自定义的属性

[ 3、重写onMesure ]

4、重写onDraw


具体实现如下


1、在res/values/  下建立一个attr_roadview.xml(名字随意), 在里面定义视图的属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="HorizontalRoadView">
        <!-- 车的图片-->
        <attr name="carBg" format="reference"/>
        <!-- 位置的图片-->
        <attr name="locationBg" format="reference"/>
        <!-- 路线地点的数目-->
        <attr name="roadNum" format="integer"/>
        <!-- 路线的颜色-->
        <attr name="lineColor" format="color"/>
        <!-- 路线的地点,用空格分开,eg 当roadnum=2时,设置为"大学城 小学城"-->
        <attr name="roadText" format="string"/>
    </declare-styleable>
</resources>
 

在布局文件中声明我们定义的View,另外添加了两个Button做测试,Button功能等下再说。需要注意的地方是 一定要引入命名空间,Android studio下命名空为:xmlns:lgx="http://schemas.android.com/apk/res-auto"

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:lgx="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="lgx.horizontalview.MainActivity">

    <lgx.horizontalview.HorizontalRoadView
        android:id="@+id/mroadView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        lgx:carBg="@drawable/schoolbus"
        lgx:locationBg="@drawable/marker"
        lgx:lineColor="#00ffff"
        lgx:roadNum="7"
        lgx:roadText="广州 大学城 大学城北 大学城南 琶洲 黄村 华南师范大学"/>
    <!-- 地点记得用" "分开-->
        />
    <Button
        android:id="@+id/nextLocation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="位置移到下一位"
        />
    <Button
        android:id="@+id/nextCar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="车移到下一位"/>
</LinearLayout>
 
2、在View的构造方法中,获取我们自定义的属性
public class HorizontalRoadView extends View {
    private static final String TAG = "HorizontalRoadView";
    private Bitmap mcarBg;
    private Bitmap mlocationBg;
    private int mroadNum;
    private int mlineColor;
    private String mroadText;
    private String[] mroadName;//保存地点名
    private int currentMyLoction;
    private int currentCarLoction;
    private Paint mPaint;
    private Rect mRect;

    private final int DEFAULT_LINE_WIDTH = px2sp(30);//线的宽度
    private final int DEFAULT_LINE_LENGTH = px2sp(200);//地点间的间隔长度
    private final int DEFAULT_TEXT_SIZE = px2sp(50);//文字的大小
    private final int BgSize = px2sp(100);//背景图大小

    private int maxTextLength = 0;//保存最长的文字长度,用于measure
    private float downx = 0, downy = 0, movex = 0, movey = 0;//触摸事件x,y
    private int currentMoveX = 0;//ontouch中x方向的偏移
    private int sumMoveX = 0;  //在x方向总的偏移

    public HorizontalRoadView(Context context) {
        this(context, null);
    }
    public HorizontalRoadView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    /*
      获取属性
     */
    public HorizontalRoadView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.HorizontalRoadView, defStyleAttr, 0);
        int n = a.getIndexCount();

        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.HorizontalRoadView_lineColor:
                    mlineColor = a.getColor(attr, Color.GREEN);
                    break;
                case R.styleable.HorizontalRoadView_carBg:
                    mcarBg = BitmapFactory.decodeResource(getResources(), a.getResourceId(attr, 0));
                    break;
                case R.styleable.HorizontalRoadView_locationBg:
                    mlocationBg = BitmapFactory.decodeResource(getResources(), a.getResourceId(attr, 0));
                    break;
                case R.styleable.HorizontalRoadView_roadNum:
                    mroadNum = a.getInt(attr, 5);// 默认5
                    break;
                case R.styleable.HorizontalRoadView_roadText:
                    mroadText = a.getString(attr);
                    break;
            }
        }
        a.recycle();
        mPaint = new Paint();//初始化画笔
        mRect = new Rect();//用于画图
        mroadName = new String[mroadNum];//初始化位置数组
        //TODO 加上判断是否为空,保证程序的稳定
        if (mroadText != null) //不为空,则将地名提取出来
            for (int j = 0; j < mroadNum; j++) {
                mroadName[j] = mroadText.split(" ")[j];//获取" "分隔开的地点
                if (mroadName[j].split("").length > maxTextLength)
                    maxTextLength = mroadName[j].split("").length;//得到最大的文字长度
            }
        //车及人的位置赋初值
        currentCarLoction = 1;
        currentMyLoction = 1;
    }

3、重写onMeasure()方法,主要是告诉系统 当长宽设置为某种模式时,你定义的视图长宽是多少
 /*
 测量出视图大小
  */
 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
     int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
     int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
     int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
 /*    if (modeWidth != MeasureSpec.EXACTLY) {//不是指定或全屏模式
        //宽度不用管
     }
*/
     if (modeHeight != MeasureSpec.EXACTLY) {//不是指定或全屏模式
         //高度等于 背景高+线宽+文字高度
         sizeHeight = BgSize + DEFAULT_LINE_WIDTH + DEFAULT_TEXT_SIZE * maxTextLength;
         heightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight, MeasureSpec.EXACTLY);
     }
     setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
 }
这里宽度不用管,当高度指定为wrap_content模式时,视图的高度是 图片背景的高度+线的宽度+文字的高度
4、重写onDraw()方法,根据获得的属性去画图。
/*
根据属性去画图
 */
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mPaint.setAntiAlias(true); // 消除锯齿
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    mPaint.setColor(mlineColor);
    mPaint.setStrokeWidth(DEFAULT_LINE_WIDTH);
    int fullWidth = mroadNum * DEFAULT_LINE_LENGTH;
    //画出直线  -.--.--.--.-
    canvas.drawLine(0, BgSize, fullWidth - sumMoveX, BgSize, mPaint);
    //画出间隔点
    mPaint.setColor(Color.WHITE);
    for (int i = 0; i < mroadNum; i++)
        canvas.drawCircle(DEFAULT_LINE_LENGTH / 2 + i * DEFAULT_LINE_LENGTH - sumMoveX, BgSize, DEFAULT_LINE_WIDTH / 2, mPaint);
    //画车
    // TODO
    mRect.set(DEFAULT_LINE_LENGTH * currentCarLoction - DEFAULT_LINE_LENGTH / 2 - BgSize / 2 - sumMoveX, 0,
            DEFAULT_LINE_LENGTH * currentCarLoction - DEFAULT_LINE_LENGTH / 2 + BgSize / 2 - sumMoveX, BgSize);
    canvas.drawBitmap(mcarBg, null, mRect, mPaint);
    //画位置
    mRect.set(DEFAULT_LINE_LENGTH * currentMyLoction - DEFAULT_LINE_LENGTH / 2 - BgSize / 2 - sumMoveX, 0,
            DEFAULT_LINE_LENGTH * currentMyLoction - DEFAULT_LINE_LENGTH / 2 + BgSize / 2 - sumMoveX, BgSize);
    canvas.drawBitmap(mlocationBg, null, mRect, mPaint);

    mPaint.setColor(Color.BLACK);
    mPaint.setTextSize(DEFAULT_TEXT_SIZE);
    mPaint.setTextAlign(Paint.Align.CENTER);//画在中心
    //画出竖的文字
    if (mroadName != null && mroadName[0] != "" && mroadName[mroadNum - 1] != "")//预防数组还没赋值,或者数量不够
        for (int i = 0; i < mroadNum; i++) {
            //Log.e(TAG, "onDraw: " + i + mroadName[i]);
            int num = mroadName[i].split("").length;//按字分开获取字长
            for (int j = 0; j < num; j++) {
                canvas.drawText(mroadName[i].split("")[j], DEFAULT_LINE_LENGTH / 2 + i * DEFAULT_LINE_LENGTH - sumMoveX,
                        j * DEFAULT_TEXT_SIZE + BgSize + DEFAULT_LINE_WIDTH, mPaint);
            }
        }
}

同时,给视图设置了触摸监听,实现滑动效果。


/**
 * 获取移动的距离
 */
@Override
public boolean onTouchEvent(MotionEvent event) {

    switch (event.getAction()) {

        case MotionEvent.ACTION_DOWN:
            downx = event.getX();
            downy = event.getY();
            //TODO
            if ((BgSize - DEFAULT_TEXT_SIZE) <= downy && downy <= (BgSize + DEFAULT_TEXT_SIZE))//在线上下一个文字范围内
            {
                currentMyLoction = (int) (downx + sumMoveX) / DEFAULT_LINE_LENGTH + 1;//最小从1开始
                postInvalidate();//刷新
            }
            Log.e(TAG, "onTouchEvent: downx:" + downx);
            break;
        case MotionEvent.ACTION_MOVE://获取移动的距离
            movex = event.getX();
            movey = event.getY();
            Log.e(TAG, "onTouchEvent: movex" + movex);
            currentMoveX = (int) (downx - movex);//只关注x上的移动,
            downx = movex;//多次进入move,保证流畅
            sumMoveX += px2sp(currentMoveX);//保存总的x移动

            //sumMoveX最小为0,最大为总长-屏幕长
            if (sumMoveX < 0)
                sumMoveX = 0;
            else if (sumMoveX > (Math.abs(mroadNum * DEFAULT_LINE_LENGTH - px2sp(getWidth()))))
                sumMoveX = Math.abs(mroadNum * DEFAULT_LINE_LENGTH - px2sp(getWidth()));
            postInvalidate();
            break;
        case MotionEvent.ACTION_UP:
            break;

    }

    return true;//必须返回true,否则不响应ACTION_MOVE
}

需要注意的是必须返回true,不然不会响应move和up,大家可以去看一下事件分发流程。


最后再设置了方法去设置和获取 人以及车的位置,不需要的话可以不管

/**
 * @param location 设置车图片位置  1--roadNum
 */
public void setCarLocation(int location) {
    //在数目范围内设置车位置
    if (0 < location && location <= mroadNum)
        currentCarLoction = location;
    postInvalidate();//刷新
}

/**
 * 获取当前车的位置
 */
public int getCarLocation()
{
    return this.currentCarLoction;
}

/**
 * 设置当前位置
 * 1--roadNum
 */
public void setMyCurrentLocation(int location) {
    if (0 < location && location <= mroadNum)
        currentMyLoction = location;
    postInvalidate();//刷新
}

/**
 * @return 获得当前选择位置
 */
public int getMyCurrentLocation()
{
    return this.currentMyLoction;
}

由于需要适应屏幕,加了px dp sp转换的方法:
/**
 * dp -> px
 */
protected int dp2px(int dpVal) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
            dpVal, getResources().getDisplayMetrics());
}

/**
 * sp -> px
 */
protected int sp2px(int spVal) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
            spVal, getResources().getDisplayMetrics());

}
/**
 * px转sp
 */

protected int px2sp(int pxVal) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
            pxVal, getResources().getDisplayMetrics());
}

/**
 * px转dp
 */
protected float px2dp(Context context, float pxVal) {
    final float scale = context.getResources().getDisplayMetrics().density;
    return (pxVal / scale);
}

5、在Acitivy中调用测试。前面说到的两个Button就是测试view的方法用,分别控制车及人的位置向下一个移动

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    HorizontalRoadView mroadView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
       mroadView= (HorizontalRoadView) findViewById(R.id.mroadView);//实例化
        Button button = (Button) findViewById(R.id.nextLocation);
        button.setOnClickListener(this);
        Button mbutton= (Button) findViewById(R.id.nextCar);
        mbutton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId())
        {
            case R.id.nextLocation://设置人的下一个位置
               mroadView.setMyCurrentLocation(mroadView.getMyCurrentLocation()+1);
                Toast.makeText(MainActivity.this,"下一个位置 ",Toast.LENGTH_SHORT).show();
                break;
            case R.id.nextCar://设置车下一个位置
               mroadView.setCarLocation(mroadView.getCarLocation()+1);
                Toast.makeText(MainActivity.this,"下一个车站",Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

最后再看下效果图:

         


源码下载

http://download.csdn.net/detail/huixion/9689319

github地址: 

https://github.com/linhuan1994/HorizontalRoadView

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值