自定義View:畫布實現自定義View(折線圖的實現)
今天道長打算說一下用畫布實現自定義View,這是道長說的自定義View的第四種實現方式了。
第一種:是放好佈局後使用NineOldAndroid監聽動畫實現,想看一下的話點選傳送門屬性動畫(二):如何自定義View以及自定義View:側滑選單動畫實現。
第二種:是放好佈局後使用TouchEvent監聽實現,傳送門在此自定義View:側滑選單實現。
第三種:是繼承相關的View,擴充相關View的功能,傳送門在此PopWindow:基本使用與自定義PopWindow。
第三種自定義View就是擴充相關View的功能。比如自定義PopWindow要增加出現動畫或者展示方式。前兩種都是使用已經存在的佈局,一種繼承FrameLayout,另一種繼承ViewGroup。放置好位置後監聽事件實現。應該說前兩種是自定義組合View。今天說的這種方式繼承View,可以用畫布繪製各種形狀的圖形,然後監聽事件實現。這裡以折線圖的實現為例,折線圖可以左右滑動。好了我們們開車……
一、效果圖
動態圖沒有,先把效果圖放在這裡,然後繪製View。
二、繪製View
上面的效果圖都看到折線圖有網格,有橫向限制區域,有標記點,有目標點,有座標軸單位,Y軸分割為兩個區域,還可以左右滑動。
把畫布Canvas與生活中的紙張看成一樣就可以了,要知道我們們在紙張上寫東西時先寫的會被後寫的遮蓋住。所以說繪製View時要注意分層。
- 建構函式,初始化畫布,畫筆
public CanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public CanvasView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CanvasView(Context context) {
this(context, null);
}
private void init(Context context) {
mTextColorSize = sp2px(context, mTextColorSize);
mTextColorSmall = sp2px(context, mTextColorSmall);
mTrendLineSize = dp2px(context, mTrendLineSize);
mInnerCircleSize = (int) dp2px(context, mInnerCircleSize);
mOuterCircleSize = (int) dp2px(context, mOuterCircleSize);
mOuterCircleRadius = (int) dp2px(context, mOuterCircleRadius);
mInnerCircleRadius = (int) dp2px(context, mInnerCircleRadius);
mYCenterSize = (int) dp2px(context, mYCenterSize);
focusTextSize = (int) dp2px(context, focusTextSize);
mPaint = new Paint();
mPaint.setTextAlign(Align.CENTER);
mPaint.setStyle(Style.STROKE);
mPaint.setAntiAlias(true);
mInnerCirclePaint = new Paint();
mInnerCirclePaint.setTextAlign(Align.CENTER);
mInnerCirclePaint.setColor(mInnerCircleColor);
mInnerCirclePaint.setTextSize(mInnerCircleSize);
mInnerCirclePaint.setAntiAlias(true);
mInnerCirclePaint.setTextSize(mInnerCircleSize);
mOuterCirclePaint = new Paint();
mOuterCirclePaint.setTextAlign(Align.CENTER);
mOuterCirclePaint.setTextSize(mOuterCircleSize);
mOuterCirclePaint.setAntiAlias(true);
mOuterCirclePaint.setTextSize(mOuterCircleSize);
mTitlePaint = new Paint();
mTitlePaint.setTextAlign(Align.CENTER);
mTitlePaint.setTextSize(sp2px(context, 20));
mTitlePaint.setTextAlign(Align.CENTER);
mRangeTrendBackgroundPaint = new Paint();
nRangeTrendBackgroundPaint = new Paint();
mPulsePaint = new Paint();
final ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = configuration.getScaledTouchSlop();
mYTitleRect = new Rect();
nYTitleRect = new Rect();
pYTitleRect = new Rect();
mPointColors = new int[]{0xFF349800, 0xFF0082b4}; // 畫筆的顏色
mYTitleWidth = (int) dp2px(context, mYTitleWidth);
mRangeTrendColors = new int[]{0XFFDBF9CC, 0XFFDBF9CC, 0XFFDBF9CC};
nRangeTrendColors = new int[]{0XFFE0F6FF, 0XFFE0F6FF, 0XFFE0F6FF, 0XFFE0F6FF};
mPulseColors = new int[]{0XFFFFFBE4, 0XFFFFFBE4, 0XFFFFFBE4};
}
- 第一層繪製折線圖限制區域
/**
* 設定界限的區域的
*
* @param canvas
* @param paint
* @param rangeTrendColors
* @param up
* @param down
*/
private void drawBackground(Canvas canvas, Paint paint, int[] rangeTrendColors, float up, float down) {
Map<String, Object> params = getViewParams();
Rect rect = new Rect();
rect.set((int) ((Float) params.get("scrollX") + 0), (int) ((Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") * up), (int) ((Float) params.get("scrollX") + getWidth()), (int) (getHeight() - (Integer) params.get("xAxisTitleHeight") - (Integer) params.get("trendHeight") * down));
LinearGradient gradient = new LinearGradient((Float) params.get("scrollX") + getWidth(), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") * up, (Float) params.get("scrollX") + getWidth(), getHeight() - (Integer) params.get("xAxisTitleHeight") - (Integer) params.get("trendHeight") * down, rangeTrendColors, null, Shader.TileMode.CLAMP);
paint.setShader(gradient);
canvas.drawRect(rect, paint);
canvas.save();
}
效果圖如下:
- 第二層繪製表格
/**
* 繪製表格
*
* @param canvas
* @param paint
*/
private void drawForm(Canvas canvas, Paint paint) {
for (int i = 0; i < mDrawCount; i++) {
drawColumnLine(canvas, paint, 0xffd5edff, (int) mTextColorSize, i);
}
for (int i = 1; i <= mDrawCount + 1; i++) {
if (i == 1 || i == 5 || i == 6 || i == 8) {
drawRowLine(canvas, paint, 0xff7ecef9, 1, i);
} else {
drawRowLine(canvas, paint, 0xffe5e5e5, 1, i);
}
}
}
/**
* 繪製表格豎線
*
* @param canvas
* @param paint
* @param lineColor
* @param lineWith
* @param position
*/
private void drawColumnLine(Canvas canvas, Paint paint, int lineColor, int lineWith, int position) {
Map<String, Object> params = getViewParams();
paint.setColor(lineColor);
paint.setTextSize(lineWith);
canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight"), (Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1), paint);
}
/**
* 繪製表格橫線
*
* @param canvas
* @param paint
* @param lineColor
* @param lineWith
* @param position
*/
private void drawRowLine(Canvas canvas, Paint paint, int lineColor, int lineWith, int position) {
Map<String, Object> params = getViewParams();
paint.setColor(lineColor);
paint.setStrokeWidth(lineWith);
canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth - 20, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (position - 1), (Float) params.get("scrollX") + getWidth(), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (position - 1), paint);
}
效果圖如下:
- 第三層繪製網格分割區域
/**
* 分割網格
*
* @param canvas
*/
private void setSplitForm(Canvas canvas, Paint paint, int color, int width) {
Map<String, Object> params = getViewParams();
paint.setColor(color);
paint.setStrokeWidth(width);
canvas.drawRect((Float) params.get("scrollX") + 0, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (5 - 1) + 1, (Float) params.get("scrollX") + getWidth(), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (6 - 1), paint);// 長方形
}
- 第四層繪製中心標記
/**
* 繪製中心標誌
*
* @param canvas
* @param paint
* @param position
*/
private void drawCenterSign(Canvas canvas, Paint paint, int position) {
drawCenterLine(canvas, paint, position);
drawTriangle(canvas, paint, position);
}
/**
* 繪製中心線
*
* @param canvas
* @param paint
* @param position
*/
private void drawCenterLine(Canvas canvas, Paint paint, int position) {
Map<String, Object> params = getViewParams();
paint.setColor(mCenterColor); // 修改中心豎線顏色
paint.setStrokeWidth(mYCenterSize);
canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight"), (Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1) - 20, paint);
}
/**
* 畫三角形
*
* @param canvas
* @param paint
* @param position
*/
public void drawTriangle(Canvas canvas, Paint paint, int position) {
Map<String, Object> params = getViewParams();
paint.setStyle(Style.STROKE);
paint.setStrokeWidth(2);
paint.setColor(0xff7ecef9);
Path path = new Path();
path.reset();
path.moveTo((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1) - 20);// 開始座標 也就是三角形的頂點
path.lineTo((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) - 20, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1));
path.lineTo((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) + 20, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1));
path.close();
canvas.drawPath(path, paint);
// 去掉底邊
mTitlePaint.setColor(Color.WHITE);
mTitlePaint.setStrokeWidth(3);
canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) - 19, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1), (Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) + 19, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1), mTitlePaint);
}
效果圖如下:
- 第五層繪製折線
/**
* 繪製轉折線
*
* @param canvas
* @param paint
* @param canvasLine
* @param color
*/
private void drawCanvasLine(Canvas canvas, Paint paint, List<Integer> canvasLine, int color) {
Path mPath = new Path(); // 繪製趨勢圖對於的Path物件
ArrayList<Integer> LinePosition = dealCanvasData(canvasLine);
Map<String, Object> params = getViewParams();
int startPosition = LinePosition.get(0);
int endPosition = LinePosition.get(1);
paint.setColor(color);
// draw trend
if (endPosition > startPosition && endPosition > 0) {
for (int i = startPosition; i < endPosition; i++) {
int currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - canvasLine.get(i)) / mScaleValue * ((Integer) params.get("trendHeight") / 8));
// 處理為負的資料,不需要可以遮蔽
if (canvasLine.get(i) < 0) {
double Y = canvasLine.get(i) * 32.0 / 40.0;
currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - Y) / scaleValue * ((Integer) params.get("trendHeight") / 8));
}
if (i == startPosition) {
mPath.moveTo(i * mDistance, currentY);
} else {
mPath.lineTo(i * mDistance, currentY);
}
}
}
canvas.drawPath(mPath, paint);
canvas.save();
mPath.reset();
}
效果圖如下:
- 第六層繪製圓點
/**
* 繪製圓
*
* @param canvas
* @param paint
* @param canvasLine
* @param color
* @param condition
*/
private void drawCircles(Canvas canvas, Paint paint, List<Integer> canvasLine, int color, ArrayList<Integer> condition) {
ArrayList<Integer> LinePosition = dealCanvasData(canvasLine);
int startPosition = LinePosition.get(0);
int endPosition = LinePosition.get(1);
Map<String, Object> params = getViewParams();
mOuterCirclePaint.setStrokeWidth(mTrendLineSize);
mInnerCirclePaint.setStrokeWidth(mTrendLineSize);
mOuterCirclePaint.setColor(color);
if (endPosition > startPosition && endPosition > 0) {
for (int i = startPosition; i < endPosition; i++) {
int currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - canvasLine.get(i)) / mScaleValue * ((Integer) params.get("trendHeight") / 8));
// 處理為負的資料,不需要可以遮蔽
if (canvasLine.get(i) < 0) {
double Y = canvasLine.get(i) * 32.0 / 40.0;
currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - Y) / scaleValue * ((Integer) params.get("trendHeight") / 8));
}
if (canvasLine.get(i) > condition.get(1) || canvasLine.get(i) < condition.get(0)) {
drawCircle(canvas, i * mDistance, currentY); // 實心
} else {
// 下面需要對 90~140的資料處理
drawCirque(canvas, i * mDistance, currentY); // 空心
}
}
}
}
/**
* 繪製實心圓
*
* @param canvas
* @param positionX
* @param positionY
*/
private void drawCircle(Canvas canvas, int positionX, int positionY) {
canvas.drawCircle(positionX, positionY, mOuterCircleRadius, mOuterCirclePaint);
}
/**
* 繪製空心圓
*
* @param canvas
* @param positionX
* @param positionY
*/
private void drawCirque(Canvas canvas, int positionX, int positionY) {
canvas.drawCircle(positionX, positionY, mOuterCircleRadius, mOuterCirclePaint);
canvas.drawCircle(positionX, positionY, mInnerCircleRadius, mInnerCirclePaint);
}
效果圖如下:
- 第七層繪製X軸Title文字
/**
* 繪製x軸Title文字
*
* @param canvas
* @param paint
* @param data
*/
private void drawXTitle(Canvas canvas, Paint paint, List<String[]> data) {
paint.setColor(0xff888888);
paint.setTextSize(mTextColorSize);
paint.setStyle(Style.FILL);
List<Integer> maxItem = getMaxItem();
Map<String, Object> params = getViewParams();
int startPosition = ((Integer) params.get("firstPosition") - (Integer) params.get("offsetCount")) >= 0 ? ((Integer) params.get("firstPosition") - (Integer) params.get("offsetCount")) : 0;
int endPosition = maxItem.size();
if (endPosition > startPosition && endPosition > 0) {
float textBaseY_x_up = (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1) + 40;
for (int i = startPosition; i < endPosition; i++) {
drawCenterTextColor(i);
// draw x axis up
mYTitleRect.set(mDistance * (i - 1), (getHeight() - (Integer) params.get("xAxisHeight_up") - (Integer) params.get("xAxisHeight_blow") - 10), mDistance * (i + 1), getHeight() - (Integer) params.get("xAxisHeight_blow") - 10);
canvas.drawText(data.get(i)[0].toString(), mYTitleRect.centerX(), textBaseY_x_up, paint);
}
}
}
- 第八層繪製Y軸Title文字
/**
* 繪製Y軸Title文字
*
* @param canvas
* @param paint
* @param backgroundColor
* @param textColor
*/
private void drawYTitle(Canvas canvas, Paint paint, int backgroundColor, int textColor) {
Map<String, Object> params = getViewParams();
FontMetrics fontMetrics = mPaint.getFontMetrics();
float fontHeight = fontMetrics.bottom - fontMetrics.top;
// 由於折線圖是左右貫通的,Y軸Title在畫布上會造成顯示混亂,所以新增底部遮擋
paint.setColor(backgroundColor);
paint.setStyle(Style.FILL);
mYTitleRect.set((int) ((Float) params.get("scrollX") + 0), 0, (int) ((Float) params.get("scrollX") + mYTitleWidth) - 30, getHeight() - 80);
canvas.drawRect(mYTitleRect, paint);
// and y-axis values
paint.setColor(textColor);
paint.setTextSize(mTextColorSize);
//繪製Y軸值
for (int i = 0; i <= 8; i++) {
String showTitle;
if (i >= 6 && i <= 8) {
showTitle = 40 * (6 - i) + 120 + "";
} else {
showTitle = 40 + (5 - i) * 35 + "";
}
float textBaseY = ((Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (i - 1)) * 2 - (((Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (i - 1)) * 2 - fontHeight) / 2 - fontMetrics.bottom;
canvas.drawText(showTitle, ((Float) params.get("scrollX") + mYTitleWidth / 2) - 20, textBaseY, paint);
}
}
- 在onDraw中新增繪製View程式碼
@Override
protected void onDraw(Canvas canvas) {
mDistance = (getWidth() - mYTitleWidth) / mDrawCount;
currentCenter = (getWidth() - mDistance);
if (mCenterPosition == -1) {
int positionLocal = mCenterRecorded * mDistance;
scrollTo(positionLocal - currentCenter, 0); // 根據可顯示的區域 動態計算中點
mCenterPosition = 0;
}
// 設定字型、筆畫寬度
// mTitlePaint.setTypeface(Typeface.DEFAULT_BOLD);
// mTitlePaint.setStrokeWidth(4);
// draw trend background
// 設定dataOne界限的區域
drawBackground(canvas, mRangeTrendBackgroundPaint, mRangeTrendColors, RatioUp, RatioDown);
// 設定dataTwo界限的區域
drawBackground(canvas, nRangeTrendBackgroundPaint, nRangeTrendColors, colorUp, colorDown);
// 設定dataThree界限的區域
drawBackground(canvas, mPulsePaint, mPulseColors, mPulseUp, mPulseDown);
// draw form
drawForm(canvas, mTitlePaint);
// split form
setSplitForm(canvas, mTitlePaint, Color.WHITE, 2);
// draw sign
drawCenterSign(canvas, mTitlePaint, 6);
if (mPoints != null) {
// draw canvas line
drawCanvasLine(canvas, mPaint, mPoints[0], mPointColors[0]);
drawCanvasLine(canvas, mPaint, mPoints[1], mPointColors[1]);
// draw circles
ArrayList<Integer> conditionsOne = new ArrayList<>();
conditionsOne.add(90);
conditionsOne.add(140);
ArrayList<Integer> conditionsTwo = new ArrayList<>();
conditionsTwo.add(60);
conditionsTwo.add(90);
drawCircles(canvas, mPaint, mPoints[0], mPointColors[0], conditionsOne);
drawCircles(canvas, mPaint, mPoints[1], mPointColors[1], conditionsTwo);
}
if (mData != null) {
// draw canvas line
drawCanvasLine(canvas, mPaint, mData[0], mPulseColor);
// draw circles
ArrayList<Integer> conditionsThree = new ArrayList<>();
conditionsThree.add(-70);
conditionsThree.add(-10);
drawCircles(canvas, mPaint, mData[0], mPulseColor, conditionsThree);
}
//and x-axis values
if (mXAxisValues != null && mXAxisValues.size() > 0) {
drawXTitle(canvas, mTitlePaint, mXAxisValues);
}
// draw y r xis rect
drawYTitle(canvas, mTitlePaint, Color.WHITE, 0xff888888);
}
效果圖如下:
現在我們們的介面繪製完成,要記住順序,不然會遮擋。然後我們們實現監聽。
二、監聽事件,實現邏輯
- 實現onTouchEvent監聽事件邏輯
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
oldX = (int) event.getX();
if ((mIsBeingDragged)) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
invalidate();
mActivePointerId = event.getPointerId(0);
return true;
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
break;
}
final int x = (int) event.getX(activePointerIndex);
int deltaX = oldX - x;
if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaX > 0) {
deltaX -= mTouchSlop;
} else {
deltaX += mTouchSlop;
}
}
// HorizontalScrollView
if (mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
oldX = x;
mTowards = deltaX;
scrollBy(deltaX, 0);
invalidate();
if (mPCenterListener != null) {
int nextCenter = getToNextCenter();
mPCenterListener.passCenter(nextCenter);
}
}
invalidate();
return true;
default:
if (mIsBeingDragged) {
mIsBeingDragged = false;
int nextCenter = getToNextCenter();
mTowards = 0;
int halfWidth = currentCenter;
int positionLocal = nextCenter * mDistance;
scrollTo(positionLocal - halfWidth, 0);
if (mAEndListener != null) {
mAEndListener.actionEnd(nextCenter);
invalidate();
centerPosition = nextCenter;
}
} else {
}
invalidate();
return true;
}
return super.onTouchEvent(event);
}
在監聽事件中不只有左右滑動監聽,還新增了滑動過中心的監聽。這裡道長不多說了,這篇部落格還是這麼幹巴巴的,以後道長可能只貼程式碼了[手動滑稽]。有不明白的地方看Demo。現在自定義View,自定義組合View道長都說了,怎麼會不說一下自定義屬性,這個在自定義View中也是很常用的。我們們在開一篇部落格,畫布實現自定義View暫時到這裡,希望這篇部落格能為你提供一些幫助。
原始碼下載
相關文章
- Flutter自定義View的實現FlutterView
- 自定義View:自定義屬性(自定義按鈕實現)View
- 自定義view實現圓角圖片View
- 自定義view實現半圓環View
- 自定義View之顏色漸變折線圖View
- 自定義View:側滑選單實現View
- 深入瞭解View實現原理以及自定義View詳解View
- 自定義view之實現日曆介面(一)View
- 自定義view之實現日曆介面(二)View
- 自定義View:側滑選單動畫實現View動畫
- 【朝花夕拾】Android自定義View篇之(四)自定義View的三種實現方式及自定義屬性詳解AndroidView
- 『自定義View實戰』—— 仿ios圖示下載viewViewiOS
- 自定義VIEWView
- 自定義view————塗鴉畫板View
- flutter-簡單實現找妹子自定義viewFlutterView
- Android自定義View實現文字輪播效果AndroidView
- Android自定義view實現數字時鐘AndroidView
- 自定義View實現箭頭沿圓轉動View
- 自定義View實用小技巧View
- Android 自定義 View 實現橫行時間軸AndroidView
- Android自定義View:快遞時間軸實現AndroidView
- 自定義view實現超萌動感小炸彈View
- Android自定義View實現微信打飛機遊戲AndroidView遊戲
- Android自定義view之實現帶checkbox的SnackbarAndroidView
- 自定義View:實現炫酷的點贊特效(仿即刻)View特效
- 自定義View公式View公式
- Android自定義View:View(二)AndroidView
- 自定義View事件之進階篇(四)-自定義Behavior實戰View事件
- Android 自定義 View 之 實現一個多功能的 IndicatorViewAndroidViewIndicator
- 自定義圓形View:實現跟隨手指移動的小球View
- 安卓自定義View實現圖片上傳進度顯示(仿QQ)安卓View
- Android 自定義 View 實戰之 PuzzleViewAndroidView
- Android 自定義 View 實戰之 StickerViewAndroidView
- 自定義View之SwitchViewView
- 自定義音量提示 viewView
- Android 自定義viewAndroidView
- Android: 自定義ViewAndroidView
- # 自定義view————流程位置View