Android Goldmap 开发(三)地图操作简单
最编程
2024-03-28 16:27:46
...
一、概述
上一节中我们了解到地图的定位,图层切换,离线地图等基础操作,接下来学习地图的基本操作。
二、本章内容
--- 地图交互设置
--- 地图绘制
1.地图交互设置
在使用地图的时候,不可避免的会涉及到与地图交互问题,如滑动手势,地图缩放,地图旋转,地图logo位置等。我们可以根据需要来决定是否开启某些交互功能。主要用到的就是高德地图API中的 UiSettings 类。通过AMap实例可以获取到UiSettings的实例 uiSettings = aMap.getUiSettings();
①手势设置
//开启放缩手势
uiSettings.setZoomGesturesEnabled(true);
//开启滑动手势
uiSettings.setScrollGesturesEnabled(true);
//开启旋转手势
uiSettings.setRotateGesturesEnabled(true);
//开启双指倾斜手势
uiSettings.setTiltGesturesEnabled(true);
//开启全部手势
uiSettings.setAllGesturesEnabled(true);
//指定手势中心点
//aMap.setPointToCenter(100,100);
//开启中心为手势中心
uiSettings.setGestureScaleByMapCenter(true);
上面这些功能在一般情况下,默认是开启的。当我们有特殊的需求时可以适当的关闭某些手势。
②地图缩放
//是否允许显示地图缩放按钮
uiSettings.setZoomControlsEnabled(true);
//是否允许收拾手势缩放地图
uiSettings.setZoomGesturesEnabled(true);
//设置双击地图放大在地图中心位置放大,false则是在点击位置放大
uiSettings.setZoomInByScreenCenter(true);
//地图缩放按钮的位置
uiSettings.setZoomPosition(AMapOptions.ZOOM_POSITION_RIGHT_BUTTOM);
//AMapOptions.ZOOM_POSITION_RIGHT_CENTER
//AMapOptions.ZOOM_POSITION_RIGHT_BUTTOM
//获取地图缩放按钮位置
Log.e(TAG, "settingZoom: " + uiSettings.getZoomPosition());
③将屏幕中心移动到指定经纬度
//这个类就是设置地图移动的参数,CameraPosition,参数1---要移动到的经纬度,
//参数2---地图的放缩级别zoom,参数3---地图倾斜度,参数4---地图的旋转角度
CameraUpdate mCameraUpdate = CameraUpdateFactory.newCameraPosition(
new CameraPosition(new LatLng(30.67, 104.07), 10, 0, 0));
//带动画的移动,aMap添加动画监听时,会有动画效果。不添加不会开启动画
aMap.animateCamera(mCameraUpdate, 5000, new AMap.CancelableCallback() {
@Override
public void onFinish() {
}
@Override
public void onCancel() {
}
});
//不带动画的移动
aMap.moveCamera(mCameraUpdate);
④其他设置
//是否显示指南针
uiSettings.setCompassEnabled(true);
//开启比例尺
uiSettings.setScaleControlsEnabled(true);
// 设置logo位置
uiSettings.setLogoPosition(AMapOptions.LOGO_POSITION_BOTTOM_CENTER);
//AMapOptions.LOGO_POSITION_BOTTOM_LEFTLOGO(左边)
//AMapOptions.LOGO_MARGIN_BOTTOMLOGO(底部)
//AMapOptions.LOGO_MARGIN_RIGHTLOGO(右边)
//AMapOptions.LOGO_POSITION_BOTTOM_CENTER(地图底部居中)
//AMapOptions.LOGO_POSITION_BOTTOM_LEFT(地图左下角)
//AMapOptions.LOGO_POSITION_BOTTOM_RIGHT (地图右下角)
//显示默认的定位按钮
uiSettings.setMyLocationButtonEnabled(true);
// 可触发定位并显示当前位置
aMap.setMyLocationEnabled(true);
//这里设置定位为了在点击定位按钮后,显示地图定位的位置方便查看
//注意这里有个坑,在点击定位后发现定位到了默认的位置(海里面),造成这种情况并不是权限和代码的问题,
//遇到这种情况时,需要手动将GPS定位打开就OK了
MyLocationStyle mls = new MyLocationStyle();
mls.myLocationType(MyLocationStyle.LOCATION_TYPE_SHOW);
aMap.setMyLocationEnabled(true);
aMap.setMyLocationStyle(mls);
代码
public class InteractiveMapActivity extends BaseActivity {
@BindView(R.id.a_map_view)
MapView aMapView;
@BindView(R.id.button)
Button button;
@BindView(R.id.button2)
Button button2;
@BindView(R.id.button3)
Button button3;
@BindView(R.id.button4)
Button button4;
private Unbinder binder;
private UiSettings uiSettings;
private AMap aMap;
private static final String TAG = "CF";
@Override
public void setContentView(@Nullable Bundle savedInstanceState) {
setContentView(R.layout.acticity_interactive);
binder = ButterKnife.bind(this);
aMapView.onCreate(savedInstanceState);
}
@Override
public void initData() {
aMap = aMapView.getMap();
uiSettings = aMap.getUiSettings();
//设置地图缩放
settingZoom();
//是否显示指南针
uiSettings.setCompassEnabled(true);
//开启比例尺
uiSettings.setScaleControlsEnabled(true);
// 设置logo位置
uiSettings.setLogoPosition(AMapOptions.LOGO_POSITION_BOTTOM_CENTER);
//AMapOptions.LOGO_POSITION_BOTTOM_LEFTLOGO(左边)
//AMapOptions.LOGO_MARGIN_BOTTOMLOGO(底部)
//AMapOptions.LOGO_MARGIN_RIGHTLOGO(右边)
//AMapOptions.LOGO_POSITION_BOTTOM_CENTER(地图底部居中)
//AMapOptions.LOGO_POSITION_BOTTOM_LEFT(地图左下角)
//AMapOptions.LOGO_POSITION_BOTTOM_RIGHT (地图右下角)
//显示默认的定位按钮
uiSettings.setMyLocationButtonEnabled(true);
// 可触发定位并显示当前位置
aMap.setMyLocationEnabled(true);
//这里设置定位为了在点击定位按钮后,显示地图定位的位置方便查看
//注意这里有个坑,在点击定位后发现定位到了默认的位置(海里面),造成这种情况并不是权限和代码的问题,
//遇到这种情况时,需要手动将GPS定位打开就OK了
MyLocationStyle mls = new MyLocationStyle();
mls.myLocationType(MyLocationStyle.LOCATION_TYPE_SHOW);
aMap.setMyLocationEnabled(true);
aMap.setMyLocationStyle(mls);
setGestures();
}
private void setGestures() {
//开启放缩手势
uiSettings.setZoomGesturesEnabled(true);
//开启滑动手势
uiSettings.setScrollGesturesEnabled(true);
//开启旋转手势
uiSettings.setRotateGesturesEnabled(true);
//开启双指倾斜手势
uiSettings.setTiltGesturesEnabled(true);
//开启全部手势
uiSettings.setAllGesturesEnabled(true);
//指定手势中心点
// aMap.setPointToCenter(100,100);
//开启中心为手势中心
uiSettings.setGestureScaleByMapCenter(true);
}
private void settingZoom() {
//是否允许显示地图缩放按钮
uiSettings.setZoomControlsEnabled(true);
//是否允许收拾手势缩放地图
uiSettings.setZoomGesturesEnabled(true);
//设置双击地图放大在地图中心位置放大,false则是在点击位置放大
uiSettings.setZoomInByScreenCenter(true);
//地图缩放按钮的位置
uiSettings.setZoomPosition(AMapOptions.ZOOM_POSITION_RIGHT_BUTTOM);
// AMapOptions.ZOOM_POSITION_RIGHT_CENTER
// AMapOptions.ZOOM_POSITION_RIGHT_BUTTOM
//获取地图缩放按钮位置
Log.e(TAG, "settingZoom: " + uiSettings.getZoomPosition());
}
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onResume() {
super.onResume();
aMapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
aMapView.onPause();
}
@Override
protected void onStop() {
super.onStop();
}
@Override
protected void onDestroy() {
aMapView.onDestroy();
binder.unbind();
super.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
aMapView.onSaveInstanceState(outState);
}
private void moveToTargetPosition() {
//这个类就是设置地图移动的参数,CameraPosition,参数1---要移动到的经纬度,参数2---地图的放缩级别zoom,参数3---地图倾斜度,参数4---地图的旋转角度
CameraUpdate mCameraUpdate = CameraUpdateFactory.newCameraPosition(
new CameraPosition(new LatLng(30.67, 104.07), 10, 0, 0));
//带动画的移动
aMap.animateCamera(mCameraUpdate, 5000, new AMap.CancelableCallback() {
@Override
public void onFinish() {
}
@Override
public void onCancel() {
}
});
//不带动画的移动
aMap.moveCamera(mCameraUpdate);
}
@OnClick({R.id.button, R.id.button2, R.id.button3, R.id.button4})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.button:
moveToTargetPosition();
break;
case R.id.button2:
showCustomMapBounds(true);
break;
case R.id.button3:
showCustomMapBounds(false);
break;
case R.id.button4:
screenCapture();
break;
}
}
private void showCustomMapBounds(boolean enable) {
if (enable) {
LatLng southwestLatLng = new LatLng(30.67, 104.07);
LatLng northeastLatLng = new LatLng(30.77, 104.17);
LatLngBounds latLngBounds = new LatLngBounds(southwestLatLng, northeastLatLng);
aMap.setMapStatusLimits(latLngBounds);
} else {
aMap.setMapStatusLimits(null);
}
}
/** 地图截屏 */
private void screenCapture() {
aMap.getMapScreenShot(new AMap.OnMapScreenShotListener() {
@Override
public void onMapScreenShot(Bitmap bitmap) {
}
@Override
public void onMapScreenShot(Bitmap bitmap, int i) {
if (null == bitmap) {
return;
}
try {
FileOutputStream fos = new FileOutputStream(
Environment.getExternalStorageDirectory() + "/MyMap/"
+ "test" + ".png");
boolean b = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
try {
fos.flush();
} catch (IOException e) {
e.printStackTrace();
}
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
StringBuffer buffer = new StringBuffer();
if (b)
buffer.append("截屏成功 ");
else {
buffer.append("截屏失败 ");
}
if (i != 0)
buffer.append("地图渲染完成,截屏无网格");
else {
buffer.append("地图未渲染完成,截屏有网格");
}
Toast.makeText(InteractiveMapActivity.this, buffer.toString(), Toast.LENGTH_SHORT).show();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
});
}
}
2.地图绘制
①绘制marker
//添加一个默认的marker
LatLng latLng = new LatLng(30.67, 104.07);
MarkerOptions markerOptions = new MarkerOptions()
//必须,设置经纬度
.position(latLng)
//设置title
.title("成都")
//设置内容
.snippet("marker内容");
aMap.addMarker(markerOptions);
//添加一个自定义的marker
LatLng latLng1 = new LatLng(30.67, 104.07);
MarkerOptions mo = new MarkerOptions();
mo.position(latLng1)
//设置透明度
.alpha(0.6f)
//设置title
.title("自定义标题")
//设置内容
.snippet("自定义内容")
//设置锚点,锚点是marker图标的位置,(0,0)-(1,1)
.anchor(0.5f, 1.0f)
//是否可见
.visible(true)
//是否可拖动,注意这里需要长按marker后,才可以拖动
.draggable(true)
//添加自定义marker图标
.icon(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(
getResources(), R.drawable.location)))
//是否平贴地图,倾斜地图,感觉marker不是竖直而是粘贴在地面上
.setFlat(false)
//是否允许显示infoWindow
.infoWindowEnable(true)
//z轴方向的值,重叠
.zIndex(10)
//设置marker图片旋转角度,正北开始逆时针方向计算
.rotateAngle(30.0f)
//设置infoWindow的偏移位置
.setInfoWindowOffset(0, 0);
aMap.addMarker(mo);
//添加一个动画marker
LatLng latLng2 = new LatLng(30.68, 104.07);
MarkerOptions mo1 = new MarkerOptions().icon(BitmapDescriptorFactory.fromBitmap(
BitmapFactory.decodeResource(getResources(), R.drawable.location)))
.title("动画").snippet("生长动画").position(latLng2);
Marker m = aMap.addMarker(mo1);
final Animation animation = new ScaleAnimation(0, 1, 0, 1);
animation.setDuration(2000);
animation.setInterpolator(new AccelerateInterpolator());
m.setAnimation(animation);
m.startAnimation();
//添加一个marker的点击事件
aMap.setOnMarkerClickListener(new AMap.OnMarkerClickListener() {
@Override
public boolean onMarkerClick(Marker marker) {
//当点击的时候添加生长动画
marker.setAnimation(animation);
marker.startAnimation();
//这里在添加点击监听事件后,原来的InfoWindow被取消了,可以在回调方法中手动实现
if (marker.isInfoWindowShown()) {
marker.hideInfoWindow();
} else {
marker.showInfoWindow();
}
return true;
}
});
//添加拖拽事件,注意此回调方法只对设置了可拖拽的marker生效
aMap.setOnMarkerDragListener(new AMap.OnMarkerDragListener() {
@Override
public void onMarkerDragStart(Marker marker) {
Log.e(TAG, "onMarkerDragStart: 被拖拽的Marker标题为->" + marker.getTitle());
}
@Override
public void onMarkerDrag(Marker marker) {
}
@Override
public void onMarkerDragEnd(Marker marker) {
Log.e(TAG, "onMarkerDragEnd: 拖拽完后Maker的经纬度-> lat="
+marker.getPosition().latitude + " lng=" + marker.getPosition().longitude);
}
});
//自定义infoWindow
aMap.setInfoWindowAdapter(new AMap.InfoWindowAdapter() {
View view;
@Override
public View getInfoWindow(Marker marker) {
if (view == null) {
view = LayoutInflater.from(DrawActivity.this).inflate(R.layout.custom_info_window, null);
}
findViews(marker, view);
return view;
}
@Override
public View getInfoContents(Marker marker) {
return null;
}
});
aMap.setOnInfoWindowClickListener(new AMap.OnInfoWindowClickListener() {
@Override
public void onInfoWindowClick(Marker marker) {
Log.e(TAG, "onInfoWindowClick: 标题为:" + marker.getTitle() + " 的InfoWindow被点击了");
}
});
/**
* 在点击marker显示infoWindow是,找到相应的控件,修改显示
*/
private void findViews(Marker marker, View view) {
TextView name = view.findViewById(R.id.name);
TextView address = view.findViewById(R.id.address);
name.setText(marker.getTitle());
address.setText(marker.getSnippet());
}
②绘制线
ArrayList<LatLng> points = new ArrayList<>();
points.add(new LatLng(30.66, 104.06));
points.add(new LatLng(30.66, 104.07));
points.add(new LatLng(30.67, 104.07));
points.add(new LatLng(30.67, 104.06));
points.add(new LatLng(30.66, 104.06));
aMap.addPolyline(new PolylineOptions().addAll(points).width(10).color(Color.argb(255, 1, 1, 1)));
ArrayList<LatLng> latlngs = new ArrayList<>();
latlngs.add(new LatLng(30.6595, 104.0595));
latlngs.add(new LatLng(30.6595, 104.0705));
latlngs.add(new LatLng(30.6705, 104.0705));
latlngs.add(new LatLng(30.6705, 104.0595));
latlngs.add(new LatLng(30.6595, 104.0595));
ArrayList<Integer> a = new ArrayList<>();
a.add(0);
a.add(1);
a.add(2);
a.add(3);
//为每条线段添加纹理
ArrayList<BitmapDescriptor> b = new ArrayList<>();
b.add(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.bb)));
b.add(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.aa)));
b.add(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.bb)));
b.add(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.aa)));
aMap.addPolyline(new PolylineOptions()
//添加多有的经纬度点
.addAll(latlngs)
//绘制线宽
.width(20)
//是否开启纹理贴图
.setUseTexture(true)
//纹理贴图段的index数组
.setCustomTextureIndex(a)
//纹理贴图每段对应的纹理资源图
.setCustomTextureList(b)
//是否虚线,纹理贴图时无效
.setDottedLine(false)
//绘制成大地线
.geodesic(false)
//设置纹理样式
.setCustomTexture(BitmapDescriptorFactory.fromBitmap(
BitmapFactory.decodeResource(getResources(), R.drawable.aa)))
//设置画线的颜色
.color(Color.argb(255, 1, 1, 1))
);
③绘制面
LatLng latLng = new LatLng(30.665, 104.065);
//绘制一个圆
Circle circle = aMap.addCircle(new CircleOptions()
.center(latLng)
.radius(500)
.fillColor(Color.argb(30, 1, 1, 1))
.strokeColor(Color.argb(255, 1, 1, 1))
.strokeWidth(10)
);
circle.setStrokeDottedLineType(0);
// //绘制一个矩形,多边形跟这个一样的
// aMap.addPolygon(new PolygonOptions()
// .addAll(createRectangle(new LatLng(30.665, 104.065), 0.01, 0.01))
// .fillColor(Color.LTGRAY).strokeColor(Color.RED).strokeWidth(5));
//
// // 绘制一个椭圆
// PolygonOptions options = new PolygonOptions();
// int numPoints = 400;
// float semiHorizontalAxis = 2f;
// float semiVerticalAxis = 0.5f;
// double phase = 2 * Math.PI / numPoints;
// for (int i = 0; i <= numPoints; i++) {
// options.add(new LatLng(
// 30.665 + semiVerticalAxis * Math.sin(i * phase),
// 104.065 + semiHorizontalAxis * Math.cos(i * phase)
// ));
// }
//
// Polygon polygon = aMap.addPolygon(options
// .strokeWidth(25)
// .strokeColor(Color.argb(50, 1, 1, 1))
// .fillColor(Color.argb(50, 1, 1, 1))
// );
/**
* 生成一个长方形的四个坐标点
*/
private List<LatLng> createRectangle(LatLng center, double halfWidth,
double halfHeight) {
List<LatLng> latLngs = new ArrayList<LatLng>();
latLngs.add(new LatLng(center.latitude - halfHeight, center.longitude - halfWidth));
latLngs.add(new LatLng(center.latitude - halfHeight, center.longitude + halfWidth));
latLngs.add(new LatLng(center.latitude + halfHeight, center.longitude + halfWidth));
latLngs.add(new LatLng(center.latitude + halfHeight, center.longitude - halfWidth));
return latLngs;
}
④绘制热力图
LatLng center = new LatLng(30.665, 104.065);
//第一步准备数据
LatLng[] data = new LatLng[100];
for (int i = 0; i < data.length; i++) {
double offsetLat = Math.random();
offsetLat /= 10;
double offsetLng = Math.random();
offsetLng /= 10;
LatLng one = new LatLng(center.latitude + offsetLat, center.longitude + offsetLng);
data[i] = one;
}
int[] colors = {Color.rgb(0, 255, 0), Color.rgb(255, 0, 0)};
float[] startPoints = {0.0f, 1.0f};
Gradient gradient = new Gradient(colors, startPoints);
//初始化热力图
HeatmapTileProvider.Builder builder = new HeatmapTileProvider.Builder();
builder.data(Arrays.asList(data))
// .gradient(gradient)
;
HeatmapTileProvider provider = builder.build();
// 初始化 TileOverlayOptions
TileOverlayOptions tileOverlayOptions = new TileOverlayOptions();
// 设置瓦片图层的提供者
tileOverlayOptions.tileProvider(provider);
// 向地图上添加 TileOverlayOptions 类对象
aMap.addTileOverlay(tileOverlayOptions);
⑤绘制长方体
/**------------------------------------地图上绘制3D图形------------------------------*/
aMap.showMapText(false);
aMap.showBuildings(false);
aMap.setCustomRenderer(new CubeMapRender(aMap));
/**------------------------------------地图上绘制3D图形-----------------------------*/
public class CubeMapRender implements CustomRenderer {
private float[] translate_vector = new float[4];
public static float SCALE = 0.005F;// 缩放暂时使用这个
private LatLng center = new LatLng(30.665, 104.065);// 北京市经纬度
private Cube cube ;
private AMap aMap;
float width, height;
public CubeMapRender(AMap aMap) {
this.aMap = aMap;
aMap.moveCamera(CameraUpdateFactory.newLatLngZoom(center,15));
}
float[] mvp = new float[16];
@Override
public void onDrawFrame(GL10 gl) {
if(cube != null) {
Matrix.setIdentityM(mvp, 0);
//偏移
PointF pointF = aMap.getProjection().toOpenGLLocation(center);
Matrix.multiplyMM(mvp,0, aMap.getProjectionMatrix(),0,aMap.getViewMatrix(),0);
Matrix.translateM(mvp, 0 , pointF.x , pointF.y , 0);
int scale = 1;
Matrix.scaleM(mvp, 0 , scale, scale, scale);
cube.drawES20(mvp);
}
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
this.width = width;
this.height = height;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//创建cube
cube = new Cube(0.2f,0.2f,0.2f);
cube.initShader();
}
@Override
public void OnMapReferencechanged() {
}
}
class Cube {
ArrayList<Float> verticesList = new ArrayList<Float>();
short indices[] = {
0, 4, 5,
0, 5, 1,
1, 5, 6,
1, 6, 2,
2, 6, 7,
2, 7, 3,
3, 7, 4,
3, 4, 0,
4, 7, 6,
4, 6, 5,
3, 0, 1,
3, 1, 2,
};
//
float[] colors = {
1f, 0f, 0f, 1f, // vertex 0 red
0f, 1f, 0f, 1f, // vertex 1 green
0f, 0f, 1f, 1f, // vertex 2 blue
1f, 1f, 0f, 1f, // vertex 3
0f, 1f, 1f, 1f, // vertex 4
1f, 0f, 1f, 1f, // vertex 5
0f, 0f, 0f, 1f, // vertex 6
1f, 1f, 1f, 1f, // vertex 7
};
public Cube(float width, float height, float depth) {
// 地图坐标系比较大,将值放大以免太小看不见
width *= 10000;
height *= 10000;
depth *= 10000;
width /= 2;
height /= 2;
depth /= 2;
//尽量不要让z轴有负数出现
float vertices1[] = {
-width, -height, -0,
width, -height, -0,
width, height, -0,
-width, height, -0,
-width, -height, depth,
width, -height, depth,
width, height, depth,
-width, height, depth,
};
for (int i = 0; i < vertices1.length; i++) {
verticesList.add(vertices1[i]);
}
//index
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(indices.length * 4);
byteBuffer.order(ByteOrder.nativeOrder());
indexBuffer = byteBuffer.asShortBuffer();
indexBuffer.put(indices);
indexBuffer.position(0);
//color
ByteBuffer byteBuffer1 = ByteBuffer.allocateDirect(colors.length * 4);
byteBuffer1.order(ByteOrder.nativeOrder());
colorBuffer = byteBuffer1.asFloatBuffer();
colorBuffer.put(colors);
colorBuffer.position(0);
init();
}
private FloatBuffer vertextBuffer;
private ShortBuffer indexBuffer;
private FloatBuffer colorBuffer;
private void init() {
if (vertextBuffer == null) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(verticesList.size() * 4);
byteBuffer.order(ByteOrder.nativeOrder());
vertextBuffer = byteBuffer.asFloatBuffer();
}
vertextBuffer.clear();
for (Float f : verticesList) {
vertextBuffer.put(f);
}
vertextBuffer.position(0);
}
class MyShader {
String vertexShader = "precision highp float;\n" +
" attribute vec3 aVertex;//顶点数组,三维坐标\n" +
" attribute vec4 aColor;//颜色数组,三维坐标\n" +
" uniform mat4 aMVPMatrix;//mvp矩阵\n" +
" varying vec4 color;//\n" +
" void main(){\n" +
" gl_Position = aMVPMatrix * vec4(aVertex, 1.0);\n" +
" color = aColor;\n" +
" }";
String fragmentShader = "//有颜色 没有纹理\n" +
" precision highp float;\n" +
" varying vec4 color;//\n" +
" void main(){\n" +
" gl_FragColor = color;\n" +
" }";
int aVertex,aMVPMatrix,aColor;
int program;
public void create() {
int vertexLocation = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
int fragmentLocation = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
GLES20.glShaderSource(vertexLocation,vertexShader);
GLES20.glCompileShader(vertexLocation);
GLES20.glShaderSource(fragmentLocation,fragmentShader);
GLES20.glCompileShader(fragmentLocation);
program = GLES20.glCreateProgram();
GLES20.glAttachShader(program,vertexLocation);
GLES20.glAttachShader(program,fragmentLocation);
GLES20.glLinkProgram(program);
aVertex = GLES20.glGetAttribLocation(program, "aVertex");
aMVPMatrix = GLES20.glGetUniformLocation(program,"aMVPMatrix");
aColor = GLES20.glGetAttribLocation(program,"aColor");
}
}
MyShader shader;
public void initShader() {
shader = new MyShader();
shader.create();
}
public void drawES20(float[] mvp) {
GLES20.glUseProgram(shader.program);
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
GLES20.glEnableVertexAttribArray(shader.aVertex);
//顶点指针
GLES20.glVertexAttribPointer(shader.aVertex, 3, GLES20.GL_FLOAT, false, 0, vertextBuffer);
//颜色指针
GLES20.glEnableVertexAttribArray(shader.aColor);
GLES20.glVertexAttribPointer(shader.aColor,4, GLES20.GL_FLOAT,false,0,colorBuffer);
GLES20.glUniformMatrix4fv(shader.aMVPMatrix,1,false,mvp,0);
//开始画
GLES20.glDrawElements(GLES20.GL_TRIANGLES, indices.length, GLES20.GL_UNSIGNED_SHORT, indexBuffer);
GLES20.glDisableVertexAttribArray(shader.aVertex);
GLES20.glDisable(GLES20.GL_DEPTH_TEST);
}
}
⑥绘制平滑移动的物体
//这里选择一个公交站,获取经过改公交站的一路公交车的路线
//让小车再这个路线上移动
BusStationQuery bsq;
BusStationSearch bss;
bsq = new BusStationQuery("天华一路", "成都");
bss = new BusStationSearch(this, bsq);
bss.setOnBusStationSearchListener(new BusStationSearch.OnBusStationSearchListener() {
@Override
public void onBusStationSearched(BusStationResult busStationResult, int i) {
List<BusStationItem> data = busStationResult.getBusStations();
if (data != null && data.size() > 0) {
final LatLonPoint busStationPoint = data.get(0).getLatLonPoint();
List<BusLineItem> dataBus = data.get(0).getBusLineItems();
if (dataBus != null && dataBus.size() > 0) {
BusLineItem bli = dataBus.get(0);
BusLineQuery blq = new BusLineQuery(
bli.getBusLineId(), BusLineQuery.SearchType.BY_LINE_ID, bli.getCityCode());
blq.setPageSize(10);
blq.setPageNumber(1);
BusLineSearch busLineSearch = new BusLineSearch(getApplicationContext(), blq);
busLineSearch.setOnBusLineSearchListener(new BusLineSearch.OnBusLineSearchListener() {
@Override
public void onBusLineSearched(BusLineResult busLineResult, int i) {
List<LatLonPoint> d = busLineResult.getBusLines().get(0).getDirectionsCoordinates();
List<LatLng> driverPath = new ArrayList<>();
for (LatLonPoint one : d) {
driverPath.add(new LatLng(one.getLatitude(), one.getLongitude()));
}
moveToPosition(busStationPoint.getLatitude(), busStationPoint.getLongitude());
aMap.addPolyline(new PolylineOptions()
.addAll(driverPath)
.width(20)
//是否开启纹理贴图
.setUseTexture(true)
//绘制成大地线
.geodesic(false)
//设置纹理样式
.setCustomTexture(BitmapDescriptorFactory.fromBitmap(
BitmapFactory.decodeResource(getResources(),
R.drawable.custtexture)))
//设置画线的颜色
.color(Color.argb(200, 0, 0, 0)));
final SmoothMoveMarker smoothMarker = new SmoothMoveMarker(aMap);
// 设置滑动的图标
smoothMarker.setDescriptor(BitmapDescriptorFactory.fromResource(
R.mipmap.icon_car));
// 设置滑动的轨迹左边点
smoothMarker.setPoints(driverPath);
// 设置滑动的总时间
smoothMarker.setTotalDuration(30);
// 开始滑动
smoothMarker.startSmoothMove();
}
});
busLineSearch.searchBusLineAsyn();
}
} else {
Log.e(TAG, "onBusStationSearched: no data");
}
Log.e(TAG, "onBusStationSearched: " + i);
}
});
bss.searchBusStationAsyn();
绘制的代码
public class DrawActivity extends BaseActivity {
private static final String TAG = "CF";
@BindView(R.id.draw_map_view)
MapView drawMapView;
private Unbinder binder;
private AMap aMap;
@Override
public void setContentView(@Nullable Bundle savedInstanceState) {
setContentView(R.layout.activity_draw_map);
binder = ButterKnife.bind(this);
drawMapView.onCreate(savedInstanceState);
aMap = drawMapView.getMap();
}
@Override
public void initData() {
//绘制Marker
// drawMarker();
//绘制线段
// drawLine();
//绘制面
// drawSurface();
//绘制热力图
drawThermodynamic();
//绘制3D模型
draw3DModel();
// drawSmoothMove();
}
//绘制平滑移动
private void drawSmoothMove() {
//这里选择一个公交站,获取经过改公交站的一路公交车的路线
//让小车再这个路线上移动
BusStationQuery bsq;
BusStationSearch bss;
bsq = new BusStationQuery("天华一路", "成都");
bss = new BusStationSearch(this, bsq);
bss.setOnBusStationSearchListener(new BusStationSearch.OnBusStationSearchListener() {
@Override
public void onBusStationSearched(BusStationResult busStationResult, int i) {
List<BusStationItem> data = busStationResult.getBusStations();
if (data != null && data.size() > 0) {
final LatLonPoint busStationPoint = data.get(0).getLatLonPoint();
List<BusLineItem> dataBus = data.get(0).getBusLineItems();
if (dataBus != null && dataBus.size() > 0) {
BusLineItem bli = dataBus.get(0);
BusLineQuery blq = new BusLineQuery(bli.getBusLineId(), BusLineQuery.SearchType.BY_LINE_ID, bli.getCityCode());
blq.setPageSize(10);
blq.setPageNumber(1);
BusLineSearch busLineSearch = new BusLineSearch(getApplicationContext(), blq);
busLineSearch.setOnBusLineSearchListener(new BusLineSearch.OnBusLineSearchListener() {
@Override
public void onBusLineSearched(BusLineResult busLineResult, int i) {
List<LatLonPoint> d = busLineResult.getBusLines().get(0).getDirectionsCoordinates();
List<LatLng> driverPath = new ArrayList<>();
for (LatLonPoint one : d) {
driverPath.add(new LatLng(one.getLatitude(), one.getLongitude()));
}
moveToPosition(busStationPoint.getLatitude(), busStationPoint.getLongitude());
aMap.addPolyline(new PolylineOptions()
.addAll(driverPath)
.width(20)
//是否开启纹理贴图
.setUseTexture(true)
//绘制成大地线
.geodesic(false)
//设置纹理样式
.setCustomTexture(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.custtexture)))
//设置画线的颜色
.color(Color.argb(200, 0, 0, 0)));
final SmoothMoveMarker smoothMarker = new SmoothMoveMarker(aMap);
// 设置滑动的图标
smoothMarker.setDescriptor(BitmapDescriptorFactory.fromResource(R.mipmap.icon_car));
// 设置滑动的轨迹左边点
smoothMarker.setPoints(driverPath);
// 设置滑动的总时间
smoothMarker.setTotalDuration(30);
// 开始滑动
smoothMarker.startSmoothMove();
}
});
busLineSearch.searchBusLineAsyn();
}
} else {
Log.e(TAG, "onBusStationSearched: no data");
}
Log.e(TAG, "onBusStationSearched: " + i);
}
});
bss.searchBusStationAsyn();
}
private void draw3DModel() {
moveToDefaultPosition();
aMap.showMapText(false);
aMap.showBuildings(false);
aMap.setCustomRenderer(new CubeMapRender(aMap));
}
/**
* ----------------------------------------热力图---------------------------------------------------------
*/
private void drawThermodynamic() {
moveToDefaultPosition();
LatLng center = new LatLng(30.665, 104.065);
//第一步准备数据
LatLng[] data = new LatLng[100];
for (int i = 0; i < data.length; i++) {
double offsetLat = Math.random();
offsetLat /= 10;
double offsetLng = Math.random();
offsetLng /= 10;
LatLng one = new LatLng(center.latitude + offsetLat, center.longitude + offsetLng);
data[i] = one;
}
int[] colors = {Color.rgb(0, 255, 0), Color.rgb(255, 0, 0)};
float[] startPoints = {0.0f, 1.0f};
Gradient gradient = new Gradient(colors, startPoints);
//初始化热力图
HeatmapTileProvider.Builder builder = new HeatmapTileProvider.Builder();
builder.data(Arrays.asList(data))
// .gradient(gradient)
;
HeatmapTileProvider provider = builder.build();
// 初始化 TileOverlayOptions
TileOverlayOptions tileOverlayOptions = new TileOverlayOptions();
// 设置瓦片图层的提供者
tileOverlayOptions.tileProvider(provider);
// 向地图上添加 TileOverlayOptions 类对象
aMap.addTileOverlay(tileOverlayOptions);
}
/**
* ----------------------------------------热力图---------------------------------------------------------
* */
/**
* -----------------------------------------Marker相关----------------------------------------------------
* */
/**
* 地图上marker相关
*/
private void drawMarker() {
//添加一个默认的marker
LatLng latLng = new LatLng(30.67, 104.07);
MarkerOptions markerOptions = new MarkerOptions()
//必须,设置经纬度
.position(latLng)
//设置title
.title("成都")
//设置内容
.snippet("marker内容");
aMap.addMarker(markerOptions);
//添加一个自定义的marker
LatLng latLng1 = new LatLng(30.67, 104.07);
MarkerOptions mo = new MarkerOptions();
mo.position(latLng1)
//设置透明度
.alpha(0.6f)
//设置title
.title("自定义标题")
//设置内容
.snippet("自定义内容")
//设置锚点,锚点是marker图标的位置,(0,0)-(1,1)
.anchor(0.5f, 1.0f)
//是否可见
.visible(true)
//是否可拖动,注意这里需要长按marker后,才可以拖动
.draggable(true)
//添加自定义marker图标
.icon(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.location)))
//是否平贴地图,倾斜地图,感觉marker不是竖直而是粘贴在地面上
.setFlat(false)
//是否允许显示infoWindow
.infoWindowEnable(true)
//z轴方向的值,重叠
.zIndex(10)
//设置marker图片旋转角度,正北开始逆时针方向计算
.rotateAngle(30.0f)
//设置infoWindow的偏移位置
.setInfoWindowOffset(0, 0);
aMap.addMarker(mo);
//添加一个动画marker
LatLng latLng2 = new LatLng(30.68, 104.07);
MarkerOptions mo1 = new MarkerOptions().icon(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.location)))
.title("动画").snippet("生长动画").position(latLng2);
Marker m = aMap.addMarker(mo1);
final Animation animation = new ScaleAnimation(0, 1, 0, 1);
animation.setDuration(2000);
animation.setInterpolator(new AccelerateInterpolator());
m.setAnimation(animation);
m.startAnimation();
//添加一个marker的点击事件
aMap.setOnMarkerClickListener(new AMap.OnMarkerClickListener() {
@Override
public boolean onMarkerClick(Marker marker) {
//当点击的时候添加生长动画
marker.setAnimation(animation);
marker.startAnimation();
//这里在添加点击监听事件后,原来的InfoWindow被取消了,可以在回调方法中手动实现
if (marker.isInfoWindowShown()) {
marker.hideInfoWindow();
} else {
marker.showInfoWindow();
}
return true;
}
});
//添加拖拽事件,注意此回调方法只对设置了可拖拽的marker生效
aMap.setOnMarkerDragListener(new AMap.OnMarkerDragListener() {
@Override
public void onMarkerDragStart(Marker marker) {
Log.e(TAG, "onMarkerDragStart: 被拖拽的Marker标题为->" + marker.getTitle());
}
@Override
public void onMarkerDrag(Marker marker) {
}
@Override
public void onMarkerDragEnd(Marker marker) {
Log.e(TAG, "onMarkerDragEnd: 拖拽完后Maker的经纬度-> lat=" + marker.getPosition().latitude + " lng=" + marker.getPosition().longitude);
}
});
//自定义infoWindow
aMap.setInfoWindowAdapter(new AMap.InfoWindowAdapter() {
View view;
@Override
public View getInfoWindow(Marker marker) {
if (view == null) {
view = LayoutInflater.from(DrawActivity.this).inflate(R.layout.custom_info_window, null);
}
findViews(marker, view);
return view;
}
@Override
public View getInfoContents(Marker marker) {
return null;
}
});
aMap.setOnInfoWindowClickListener(new AMap.OnInfoWindowClickListener() {
@Override
public void onInfoWindowClick(Marker marker) {
Log.e(TAG, "onInfoWindowClick: 标题为:" + marker.getTitle() + " 的InfoWindow被点击了");
}
});
moveToDefaultPosition();
}
private void moveToDefaultPosition() {
//移动到marker位置
CameraUpdate mCameraUpdate = CameraUpdateFactory.newCameraPosition(
new CameraPosition(
new LatLng(30.665, 104.065),
15,
0,
0
)
);
aMap.moveCamera(mCameraUpdate);
}
private void moveToPosition(double lat, double lng) {
//移动到marker位置
CameraUpdate mCameraUpdate = CameraUpdateFactory.newCameraPosition(
new CameraPosition(
new LatLng(lat, lng),
13,
0,
0
)
);
aMap.moveCamera(mCameraUpdate);
}
/**
* 在点击marker显示infoWindow是,找到相应的控件,修改显示
*/
private void findViews(Marker marker, View view) {
TextView name = view.findViewById(R.id.name);
TextView address = view.findViewById(R.id.address);
name.setText(marker.getTitle());
address.setText(marker.getSnippet());
}
/**
* -----------------------------------------Marker相关----------------------------------------------------
*/
/**
* -----------------------------------------Line相关------------------------------------------------------
*/
private void drawLine() {
moveToDefaultPosition();
ArrayList<LatLng> points = new ArrayList<>();
points.add(new LatLng(30.66, 104.06));
points.add(new LatLng(30.66, 104.07));
points.add(new LatLng(30.67, 104.07));
points.add(new LatLng(30.67, 104.06));
points.add(new LatLng(30.66, 104.06));
aMap.addPolyline(new PolylineOptions().addAll(points).width(10).color(Color.argb(255, 1, 1, 1)));
ArrayList<LatLng> latlngs = new ArrayList<>();
latlngs.add(new LatLng(30.6595, 104.0595));
latlngs.add(new LatLng(30.6595, 104.0705));
latlngs.add(new LatLng(30.6705, 104.0705));
latlngs.add(new LatLng(30.6705, 104.0595));
latlngs.add(new LatLng(30.6595, 104.0595));
ArrayList<Integer> a = new ArrayList<>();
a.add(0);
a.add(1);
a.add(2);
a.add(3);
ArrayList<BitmapDescriptor> b = new ArrayList<>();
b.add(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.bb)));
b.add(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.aa)));
b.add(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.bb)));
b.add(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.aa)));
aMap.addPolyline(new PolylineOptions()
//添加多有的经纬度点
.addAll(latlngs)
//绘制线宽
.width(20)
//是否开启纹理贴图
.setUseTexture(true)
//纹理贴图段的index数组
.setCustomTextureIndex(a)
//纹理贴图每段对应的纹理资源图
.setCustomTextureList(b)
//是否虚线,纹理贴图时无效
.setDottedLine(false)
//绘制成大地线
.geodesic(false)
//设置纹理样式
.setCustomTexture(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.aa)))
//设置画线的颜色
.color(Color.argb(255, 1, 1, 1))
);
}
/**
* -----------------------------------------Line相关------------------------------------------------------
*/
/**
* -----------------------------------------绘制几何图形相关------------------------------------------------------
*/
private void drawSurface() {
moveToDefaultPosition();
LatLng latLng = new LatLng(30.665, 104.065);
//绘制一个圆
Circle circle = aMap.addCircle(new CircleOptions()
.center(latLng)
.radius(500)
.fillColor(Color.argb(30, 1, 1, 1))
.strokeColor(Color.argb(255, 1, 1, 1))
.strokeWidth(10)
);
circle.setStrokeDottedLineType(0);
// //绘制一个矩形,多边形跟这个一样的
// aMap.addPolygon(new PolygonOptions()
// .addAll(createRectangle(new LatLng(30.665, 104.065), 0.01, 0.01))
// .fillColor(Color.LTGRAY).strokeColor(Color.RED).strokeWidth(5));
//
// // 绘制一个椭圆
// PolygonOptions options = new PolygonOptions();
// int numPoints = 400;
// float semiHorizontalAxis = 2f;
// float semiVerticalAxis = 0.5f;
// double phase = 2 * Math.PI / numPoints;
// for (int i = 0; i <= numPoints; i++) {
// options.add(new LatLng(
// 30.665 + semiVerticalAxis * Math.sin(i * phase),
// 104.065 + semiHorizontalAxis * Math.cos(i * phase)
// ));
// }
//
// Polygon polygon = aMap.addPolygon(options
// .strokeWidth(25)
// .strokeColor(Color.argb(50, 1, 1, 1))
// .fillColor(Color.argb(50, 1, 1, 1))
// );
}
/**
* 生成一个长方形的四个坐标点
*/
private List<LatLng> createRectangle(LatLng center, double halfWidth,
double halfHeight) {
List<LatLng> latLngs = new ArrayList<LatLng>();
latLngs.add(new LatLng(center.latitude - halfHeight, center.longitude - halfWidth));
latLngs.add(new LatLng(center.latitude - halfHeight, center.longitude + halfWidth));
latLngs.add(new LatLng(center.latitude + halfHeight, center.longitude + halfWidth));
latLngs.add(new LatLng(center.latitude + halfHeight, center.longitude - halfWidth));
return latLngs;
}
/**
* -----------------------------------------绘制几何图形相关------------------------------------------------------
*/
@Override
protected void onResume() {
super.onResume();
drawMapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
drawMapView.onPause();
}
@Override
protected void onStop() {
super.onStop();
}
@Override
protected void onDestroy() {
List<Marker> list = aMap.getMapScreenMarkers();
if (list != null && list.size() > 0) {
for (Marker one : list) {
one.destroy();
}
}
drawMapView.onDestroy();
//这里注意,butterknife注销一定要在所有销毁的最后,否则会出现空指针异常
binder.unbind();
super.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
drawMapView.onSaveInstanceState(outState);
}
}
推荐阅读
-
像首席技术官一样思考:如何高效管理 30 人的研发团队?-管理越多越轻松。好的研发团队,应该是上拨下用,即下级对上级的向上管理;而不是反过来,总是向下管理,甚至是 CTO 做经理的事,经理做工程师的事,工程师最终会被当成实习生。如果是这样,就会越管越累,不仅团队无法成长,而且团队整天很忙还效率低下,问题一大堆。 有这样一个小故事:一位高级经理下班后帮忙倒垃圾,结果被老板训斥了一顿。这就好比首席技术官做了实习生自己该做的事。事情本身没有对错之分,只是从不同的角度有不同的理解。 古人云:"用人不疑,疑人不用"。在面对自己的研发团队时,应该相信他们能做好,授权一线开发人员充分发挥专业特长,不要限制他们的工作。但在相信他们的同时,也要进行二次确认,始终秉持 "我相信,但我要确认 "的原则和严谨的精神。因为每个人都会犯错和疏忽,通过发挥团队的智慧,团队犯错的机会就会大大减少。比如回归测试、代码审查、开发演示、变更审批等等。 如前所述,每个人都难免会犯错。但作为管理者,你所设计和商定的流程不能出错。管理者的每一个决定和沟通都应该经过深思熟虑。就像红绿灯的交通设计,某辆车不小心闯红灯可能会扣分,但红绿灯的设计一定要正确、人性化、统一。再比如,开发人员可能会因为疏忽大意写出 bug,但研发流程的设计和上线流程的发布不能有任何差错。因此,流程体系的设计,一方面要结合当前团队规模、业务特点和需要重点解决的问题来设计,另一方面也要在人员防错、效率提升、发挥团队集体智慧等维度进行综合考量。应该站在更高更抽象的角度去思考,不断思考一个倍受欢迎的园区应该如何设计,思考一个灵动、经典、永恒的建筑应该遵循怎样的模式,思考一个成功、优秀、卓越的研发团队应该需要怎样的流程和制度。 最后,反馈很重要。向上汇报很重要,向下反馈也很重要。能够保持顺畅的双向反馈和闭环管理,对研发团队的协作和沟通有着非常明显的积极作用。在向上汇报方面,要培养团队在正式汇报、会议汇报、私下沟通、书面总结、非正式场合等方面的沟通能力,提醒下属报喜也要报忧。凡事先记录,再跟进,最后反馈。反馈很重要,主动汇报更难得。 另一方面,同时也不要忽视向下反馈。好的爱,是双向的。团队也是如此,没有严格的上下级之分,只是分工和角色不同而已。作为管理者,不必总保持一种 "神秘感",让人 "捉摸不透 "才是牛。当团队做得好或有人做得好时,要记得在公开或私下场合给予肯定和赞许。业务有增长、业绩有提升时,别忘了给团队一些鼓励,或者安排一次下午茶或聚餐。在例会或正式会议上,也可以同步向大家传达一些重要信息和高层指示。"欲速则不达,欲远则同行"。 当向上汇报、向下反馈的沟通闭环形成后,同时结合前面研发过程的管理闭环,双管齐下,就能形成良性循环。如此反复,持之以恒,优秀卓越的研发团队,必将呈现。 能力、产出和效率 接下来,继续重复关于能力、产出和效率的话题。 站在不同的角色,以及一个企业经营、生存和发展所需要的基础上,我把研发生产力分为三个层次,分别是:一线员工关心的研发能力、管理层关心的软件产出和操作人员关心的企业生产效率。简单概括就是:既要把工作做好,又要能出成果,还要能帮企业赚钱。
-
纯干货分享 | 研发效能提升——敏捷需求篇-而敏捷需求是提升效能的方式中不可或缺的模块之一。 云智慧的敏捷教练——Iris Xu近期在公司做了一场分享,主题为「敏捷需求挖掘和组织方法,交付更高业务价值的产品」。Iris具有丰富的团队敏捷转型实施经验,完成了企业多个团队从传统模式到敏捷转型的落地和实施,积淀了很多的经验。 这次分享主要包含以下2个部分: 第一部分是用户影响地图 第二部分是事件驱动的业务分析Event driven business analysis(以下简称EDBA) 用户影响地图,是一种从业务目标到产品需求映射的需求挖掘和组织的方法。 在软件开发过程中可能会遇到一些问题,比如大家使用不同的业务语言、技术语言,造成角色间的沟通阻碍,还会导致一些问题,比如需求误解、需求传递错误等;这会直接导致产品的功能需求和要实现的业务目标不是映射关系。 但在交付期间,研发人员必须要将这些需求实现交付,他们实则并不清楚这些功能需求产生的原因是什么、要解决客户的哪些痛点。研发人员往往只是拿到了解决方案,需要把它实现,但没有和业务侧一起去思考解决方案是否正确,能否真正的帮助客户解决问题。而用户影响地图通常是能够连接业务目标和产品功能的一种手段。 我们在每次迭代里加入的假设,也就是功能需求。首先把它先实现,再逐步去验证我们每一个小目标是否已经实现,再看下一个目标要是什么。那影响地图就是在这个过程中帮我们不断地去梳理目标和功能之间的关系。 我们在软件开发中可能存在的一些问题 针对这些问题,我们如何避免?先简单介绍做敏捷转型的常规思路: 先做团队级的敏捷,首先把产品、开发、测试人员,还有一些更后端的人员比如交互运维的同学放在一起,组成一个特训团队做交付。这个团队要包含交付过程中所涉及的所有角色。 接着业务敏捷要打通整个业务环节和研发侧的一个交付。上图中可以看到在敏捷中需求是分层管理的,第一层是业务需求,在这个层级是以用户目标和业务目标作为输入进行规划,同时需要去考虑客户的诉求。业务人员通过获取到的业务需求,进一步的和团队一起将其分解为产品需求。所以业务需求其实是我们真正去发布和运营的单元,它可以被独立发布到我们的生产环境上。我们的产品需求其实就是产品的具体功能,它是我们集成和测试的对象,也就是我们最终去部署到系统上的一个基本单元。产品需求再到了我们的开发团队,映射到迭代计划会上要把它分解为相应的技术任务,包括我们平时所说的比如一些前端的开发、后端的开发、测试都是相应的技术任务。所以业务敏捷要达到的目标是需要去持续顺畅高质量的交付业务价值。 将这几个点串起来,形成金字塔结构。最上层我们会把业务目标放在整个金字塔的塔尖。这个业务目标是通过用户的目标以及北极星指标确立的。确认业务目标后再去梳理相应的业务流程,最后生产。另外产品需求包含了操作流程和业务规则,具需求交付时间、工程时间以及我们的一些质量标准的要求。 谈到用户影响的地图,在敏捷江湖上其实有一个传说,大家都有一个说法叫做敏捷需求的“任督二脉”。用户影响地图其实就是任脉,在黑客马拉松上用过的用户故事地图其实叫督脉。所以说用户影响地图是在用户故事地图之前,先帮我们去梳理出我们要做哪些东西。当我们真正识别出我们要实现的业务活动之后,用户故事地图才去梳理我们整个的业务工作流,以及每个工作流节点下所要包含的具体功能和用户故事。所以说用户影响地图需要解决的问题,我们包括以下这些: 首先是范围蔓延,我们在整张地图上,功能和对应的业务目标是要去有一个映射的。这就避免了一些在我们比如有很多干系人参与的会议上,那大家都有不同想法些立场,会提出很多需求(正确以及错误的需求)。这个时候我们会依据目标去看这些需求是否真的是会影响我们的目标。 这里提到的错误需求,比如是利益相关的人提出的、客户认为产品应该有的、某个产品经理需求分析师认为可以有的....但是这些功能在用户影响地图中匹配不到对应目标的话,就需要降低优先级或弃掉。另外,通常我们去制定解决方案的时候,会考虑较完美的实现,导致解决方案括很多的功能。这个时候关键目标至关重要,会帮助我们梳理筛选、确定优先级。 看一下用户影响到地图概貌 总共分为一个三层的结构: 第一层why,你的业务目标哪个是最重要的,为什么?涉及到的角色有哪些? 第二层how ,怎样产生影响?影响用户角色什么样的行为? (不需要去列出所有的影响,基于业务目标) 第三层what,最关键的是在梳理需求时不需一次把所有细节想全,这通常团队中经常遇到的问题。 我们用这个例子来看一下 这是一个客服中心的影响地图,业务目标是 3个月内不增加客服人数的前提下能支持1.5倍的用户数。此业务目标设定是符合 smart 原则的,specific非常的具体,miserable 是可以衡量的,action reoriented是面向活动的, real list 也是很实际的。 量化的目标会指引我们接下来的行动,梳理一个业务目标,尽量去量化,比如 :我们通过打造一条什么样的流水线,能够提高整个部署的效率,时间是原来的 1/2 。这样才是一个能量化的有意义的目标。 回到这幅图, how 层级识别出来的内容,客服角色:想要对它施加的影响,把客户引导到论坛上,帮助客户更容易的跟踪问题,更快速的去定位问题。初级用户:方论坛上找到问题。高级用户:在论坛上回答问题。通过我们这些用户角色,进行活动,完成在不增加客户客服人数的前提下支持更多的用户数量。 最后一个层级,才是我们日常接触比较多的真正的功能的特性和需求,比如引导到客户到论坛上,其实这个产品就需要有一个常见问题的论坛的链接。这个层次需要我们团队进一步地在交付,在每个迭代之前做进一步的梳理,细化成相应的用户故事。 这个是云智慧团队中,自己做的影响地图的范例,可以看下整个的层级结构。序号表示优先级。 那我们用户影响地图可以总结为:
-
Android 开发 - Goldmap SDK 简单开发
-
关于 Android Goldmap 简单开发示例代码 (DEMO)
-
Android Goldmap 开发(三)地图操作简单
-
Android Goldmap 开发(四)自定义离线地图下载
-
Android 开发中 nodpi、xhdpi、hdpi、mdpi、ldpi 的概念 - 术语和概念 屏幕尺寸 屏幕的物理尺寸,基于屏幕的对角线长度(如 2.8 英寸、3.5 英寸)。 简而言之,安卓系统将所有屏幕尺寸简化为三大类:大、普通和小。 程序可以为这三种屏幕尺寸提供三种不同的布局选项,然后系统会以合适的方式将布局选项呈现到相应的屏幕上,这个过程不需要程序员用代码进行干预。 屏幕纵横比 屏幕的物理长度与物理宽度之比。程序只需使用系统提供的资源分类器 long(长)和 notlong(不长),就能为具有特定长宽比的屏幕提供配制材料。 分辨率 屏幕的像素总数。请注意,分辨率并不意味着长宽比,尽管在大多数情况下,分辨率表示为 "宽度 x 长度"。在安卓系统中,程序一般不直接处理分辨率。 密度 根据屏幕分辨率,沿屏幕宽度和长度排列的像素数量。 密度较低的屏幕在长度和宽度方向上的像素都相对较少,而密度较高的屏幕通常会在同一区域内排列很多甚至非常非常多的像素。屏幕的密度非常重要;例如,一个界面元素(如按钮)的长度和宽度以像素为单位,在低密度屏幕上会显得很大,但在高密度屏幕上就会显得很小。 独立于密度的像素(DIP)是指程序用来定义界面元素的抽象意义上的像素。它作为一个与实际密度无关的单位,帮助程序员构建布局方案(界面元素的宽度、高度和位置)。 与密度无关的像素在逻辑上与像素密度为 160 DPI 的屏幕上的像素大小相同,而 160 DPI 是安卓平台默认的显示设备。在运行时,平台会以目标屏幕的密度为基准,"透明 "地处理所有所需的 DIP 缩放操作。要将与密度无关的像素转换为屏幕像素,可以使用一个简单的公式:像素 = DIP * (密度 / 160)。例如,在 240 DPI 的屏幕上,1 个 DIP 等于 1.5 个物理像素。强烈建议使用 DIP 来定义程序界面的布局,因为这样可以确保用户界面在所有分辨率的屏幕上都能正常显示。 为了简化程序员在面对各种分辨率时的麻烦,也为了让各种分辨率的平台都能直接运行这些程序,Android 平台将所有屏幕以密度和分辨率作为分类方式,分别分为三类:- 三大尺寸:大、普通、小;- 三种不同密度:高(hdpi)、中(mdpi)和低(ldpi)。DPI 表示 "每英寸点数",即每英寸的像素数。如果需要,程序可以为不同的屏幕尺寸提供不同的资源(主要是布局),为不同的屏幕密度提供不同的资源(主要是位图)。除此之外,程序无需对屏幕尺寸或密度进行任何额外处理。执行时,平台会根据屏幕本身的尺寸和密度特性自动加载相应的资源,并将其从逻辑像素(DIP,用于定义界面布局)转换为屏幕上的物理像素。
-
Android Goldmap 开发(V)获取地图数据
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面