热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

Android自定义view制作绚丽的验证码

这篇文章主要介绍了Android自定义view制作绚丽的验证码的相关资料,需要的朋友可以参考下

废话不多说了,先给大家展示下自定义view效果图,如果大家觉得还不错的话,请继续往下阅读。

这里写图片描述

怎么样,这种验证码是不是很常见呢,下面我们就自己动手实现这种效果,自己动手,丰衣足食,哈哈~

一、 自定义view的步骤

自定义view一直被认为android进阶通向高手的必经之路,其实自定义view好简单,自定义view真正难的是如何绘制出高难度的图形,这需要有好的数学功底(后悔没有好好学数学了~),因为绘制图形经常要计算坐标点及类似的几何变换等等。自定义view通常只需要以下几个步骤:

写一个类继承View类;

重新View的构造方法;

测量View的大小,也就是重写onMeasure()方法;

重新onDraw()方法。

其中第三步不是必须的,只有当系统无法确定自定义的view的大小的时候需要我们自己重写onMeasure()方法来完成自定义view大小的测量,因为如果用户(程序员)在使用我们的自定义view的时候没有指定其精确大小(宽度或高度),如:布局文件中layout_width或layout_heigth属性值为wrap_content而不是match_parent或某个精确的值,那么系统就不知道我们自定义view在onDraw()中绘制的图形的大小,所以通常要让我们自定义view支持wrap_content那么我们就必须重写onMeasure方法来告诉系统我们要绘制的view的大小(宽度和高度)。

还有,如果我们自定义view需要一些特殊的属性,那么我们还需要自定义属性,这篇文章将会涉及到自定义属性和上面的四个步骤的内容。

二、 自定义view的实现

要实现这种验证码控件,我们需要先分析一下它要怎么实现。通过看上面的效果图,我们可以知道要实现这种效果,首先需要在绘制验证码字符串,即图中的文本部分,然后绘制一些干扰点,再就是绘制干扰线了,分析完毕。下面我们根据分析结果一步步实现这种效果。

1. 继承View,重写构造方法

写一个类继承View,然后重新它的构造方法

/**
* Created by lt on 2016/3/2.
*/
public class ValidationCode extends View{
/**
* 在java代码中创建view的时候调用,即new
* @param context
*/
public ValidationCode(Context context) {
this(context,null);
}

/**
* 在xml布局文件中使用view但没有指定style的时候调用
* @param context
* @param attrs
*/
public ValidationCode(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

/**
* 在xml布局文件中使用view并指定style的时候调用
* @param context
* @param attrs
* @param defStyleAttr
*/
public ValidationCode(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 做一些初始化工作
init();
}
}

View有三个构造方法,一般的做法都是让一个参数和两个参数的构造方法调用三个构造参数的方法,这三个构造方法的调用情况看方法上面的注释。在这个构造方法里面我们先做一些初始化随机验证码字符串,画笔等工作:

/**
* 初始化一些数据
*/
private void init() {
// 生成随机数字和字母组合
mCodeString = getCharAndNumr(mCodeCount);
// 初始化文字画笔
mTextPaint = new Paint();
mTextPaint.setStrokeWidth(3); // 画笔大小为3
mTextPaint.setTextSize(mTextSize); // 设置文字大小
// 初始化干扰点画笔
mPointPaint = new Paint();
mPointPaint.setStrokeWidth(6);
mPointPaint.setStrokeCap(Paint.Cap.ROUND); // 设置断点处为圆形
// 初始化干扰线画笔
mPathPaint = new Paint();
mPathPaint.setStrokeWidth(5);
mPathPaint.setColor(Color.GRAY);
mPathPaint.setStyle(Paint.Style.STROKE); // 设置画笔为空心
mPathPaint.setStrokeCap(Paint.Cap.ROUND); // 设置断点处为圆形
// 取得验证码字符串显示的宽度值
mTextWidth = mTextPaint.measureText(mCodeString);
}

到这里,我们就完成了自定义View步骤中的前面的两小步了,接下来就是完成第三步,即重写onMeasure()进行我们自定义view大小(宽高)的测量了:

2. 重写onMeasure(),完成View大小的测量

/**
* 要像layout_width和layout_height属性支持wrap_content就必须重新这个方法
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 分别测量控件的宽度和高度,基本为模板方法
int measureWidth = measureWidth(widthMeasureSpec);
int measureHeight = measureHeight(heightMeasureSpec);
// 其实这个方法最终会调用setMeasuredDimension(int measureWidth,int measureHeight);
// 将测量出来的宽高设置进去完成测量
setMeasuredDimension(measureWidth, measureHeight);
}

测量宽度的方法:

/**
* 测量宽度
* @param widthMeasureSpec
*/
private int measureWidth(int widthMeasureSpec) {
int result = (int) (mTextWidth*1.8f);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if(widthMode == MeasureSpec.EXACTLY){
// 精确测量模式,即布局文件中layout_width或layout_height一般为精确的值或match_parent
result = widthSize; // 既然是精确模式,那么直接返回测量的宽度即可
}else{
if(widthMode == MeasureSpec.AT_MOST) {
// 最大值模式,即布局文件中layout_width或layout_height一般为wrap_content
result = Math.min(result,widthSize);
}
}
return result;
}

测量高度的方法:

/**
* 测量高度
* @param heightMeasureSpec
*/
private int measureHeight(int heightMeasureSpec) {
int result = (int) (mTextWidth/1.6f);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(heightMode == MeasureSpec.EXACTLY){
// 精确测量模式,即布局文件中layout_width或layout_height一般为精确的值或match_parent
result = heightSize; // 既然是精确模式,那么直接返回测量的宽度即可
}else{
if(heightMode == MeasureSpec.AT_MOST) {
// 最大值模式,即布局文件中layout_width或layout_height一般为wrap_content
result = Math.min(result,heightSize);
}
}
return result;
}

说明:其实onMeasure()方法最终会调用setMeasuredDimension(int measureWidth,int measureHeight);将测量出来的宽高设置进去完成测量,而我们要做的就是测量得到宽度和高度的值,测量宽度和高度的方法最重要的就是得到当用户(程序员)没有给我们的控件指定精确的值(具体数值或match_parent)时合适的宽度和高度,所以,以上测量宽度和高度的方法基本上是一个模板方法,要做的就是得到result的一个合适的值,这里我们无需关注给result的那个值,因为这个值根据控件算出来的一个合适的值(也许不是很合适)。

完成了控件的测量,那么接下来我们还要完成控件的绘制这一大步,也就是自定义view的核心的一步重写onDraw()方法绘制图形。

3. 重写onDraw(),绘制图形

根据我们上面的分析,我们需要绘制验证码文本字符串,干扰点,干扰线。由于干扰点和干扰线需要坐标和路径来绘制, 所以在绘制之前先做一些初始化随机干扰点坐标和干扰线路径:

private void initData() {
// 获取控件的宽和高,此时已经测量完成
mHeight = getHeight();
mWidth = getWidth();
mPoints.clear();
// 生成干扰点坐标
for(int i=0;i<150;i++){
PointF pointF = new PointF(mRandom.nextInt(mWidth)+10,mRandom.nextInt(mHeight)+10);
mPoints.add(pointF);
}
mPaths.clear();
// 生成干扰线坐标
for(int i=0;i<2;i++){
Path path = new Path();
int startX = mRandom.nextInt(mWidth/3)+10;
int startY = mRandom.nextInt(mHeight/3)+10;
int endX = mRandom.nextInt(mWidth/2)+mWidth/2-10;
int endY = mRandom.nextInt(mHeight/2)+mHeight/2-10;
path.moveTo(startX,startY);
path.quadTo(Math.abs(endX-startX)/2,Math.abs(endY-startY)/2,endX,endY);
mPaths.add(path);
}
}

有了这些数据之后,我们可以开始绘制图形了。

(1)绘制验证码文本字符串

由于验证码文本字符串是随机生成的,所以我们需要利用代码来随机生成这种随机验证码:

/**
* java生成随机数字和字母组合
* @param length[生成随机数的长度]
* @return
*/
public static String getCharAndNumr(int length) {
String val = "";
Random random = new Random();
for (int i = 0; i 

这种代码是java基础,相信大家都看得懂,看不懂也没关系,这种代码网上随便一搜就有,其实我也是直接从网上搜的,嘿嘿~。

android的2D图形api canvas提供了drawXXX()方法来完成各种图形的绘制,其中就有drawText()方法来绘制文本,同时还有drawPosText()在给定的坐标点上绘制文本,drawTextOnPath()在给定途径上绘制图形。仔细观察上面的效果图,发现文本有的不是水平的,即有的被倾斜了,这就可以给我们的验证码提升一定的识别难度,要实现文字倾斜效果,我们可以通过drawTextOnPath()在给定路径绘制文本达到倾斜效果,然而这种方法实现比较困难(坐标点和路径难以计算),所以,我们可以通过canvas提供的位置变换方法rorate()结合drawText()实现文本倾斜效果。

int length = mCodeString.length();
float charLength = mTextWidth/length;
for(int i=1;i<=length;i++){
int offsetDegree = mRandom.nextInt(15);
// 这里只会产生0和1,如果是1那么正旋转正角度,否则旋转负角度
offsetDegree = mRandom.nextInt(2) == 1&#63;offsetDegree:-offsetDegree;
canvas.save();
canvas.rotate(offsetDegree, mWidth / 2, mHeight / 2);
// 给画笔设置随机颜色,+20是为了去除一些边界值
mTextPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);canvas.drawText(String.valueOf(mCodeString.charAt(i - 1)), (i-1) * charLength * 1.6f+30, mHeight * 2 / 3f, mTextPaint);
canvas.restore();
}

这段代码通过for循环分别绘制验证码字符串中的每个字符,每绘制一个字符都将画布旋转一个随机的正负角度,然后通过drawText()方法绘制字符,每个字符的绘制起点坐标根据字符的长度和位置不同而不同,这个自己计算,这里也许也不是很合适。要注意的是,每次对画布canvas进行位置变换的时候都要先调用canvas.save()方法保存好之前绘制的图形,绘制结束后调用canvas.restore()恢复画布的位置,以便下次绘制图形的时候不会由于之前画布的位置变化而受影响。

(2)绘制干扰点

// 产生干扰效果1 -- 干扰点
for(PointF pointF : mPoints){
mPointPaint.setARGB(255,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20);
canvas.drawPoint(pointF.x,pointF.y,mPointPaint);
}

给干扰点画笔设置随机颜色,然后根据随机产生的点的坐标利用canvas.drawPoint()绘制点。

(3)绘制干扰线

// 产生干扰效果2 -- 干扰线
for(Path path : mPaths){
mPathPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);
canvas.drawPath(path, mPathPaint);
}

给干扰线画笔设置随机颜色,然后根据随机产生路径利用canvas.drawPath()绘制贝塞尔曲线,从而绘制出干扰线。

4. 重写onTouchEvent,定制View事件

这里做这一步是为了实现当我们点击我们的自定义View的时候,完成一些操作,即定制View事件。这里,我们需要当用户点击验证码控件的时候,改变验证码的文本字符串。

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
// 重新生成随机数字和字母组合
mCodeString = getCharAndNumr(mCodeCount);
invalidate();
break;
default:
break;
}
return super.onTouchEvent(event);
}

OK,到这里我们的这个自定义View就基本完成了,可能大家会问,这个自定义View是不是扩展性太差了,定制性太低了,说好的自定义属性呢?跑哪里去了。不要急,下面我们就来自定义我们自己View的属性,自定义属性。

5. 自定义属性,提高自定义View的可定制性

(1)在资源文件attrs.xml文件中定义我们的属性(集)

<&#63;xml version="1.0" encoding="utf-8"&#63;>





说明:

在attrs.xml文件中的attr节点中定义我们的属性,定义属性需要name属性表示我们的属性值,同时需要format属性表示属性值的格式,其格式有很多种,如果属性值可以使多种格式,那么格式间用”|”分开;

declare-styleable节点用来定义我们自定义属性集,其name属性指定了该属性集的名称,可以任意,但一般为自定义控件的名称;

如果属性已经定义了(如layout_width),那么可以直接引用该属性,不要指定格式了。

(2)在布局文件中引用自定义属性,注意需要引入命名空间



引入命名空间在现在只需要添加xmlns:lt="http://schemas.android.com/apk/res-auto"即可(lt换成你自己的命名空间名称),而在以前引入命名空间方式为xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01",res后面的包路径指的是项目的package`

(3)在构造方法中获取自定义属性的值

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndentifyingCode);
mCodeCount = typedArray.getInteger(R.styleable.IndentifyingCode_codeCount, 5); // 获取布局中验证码位数属性值,默认为5个
// 获取布局中验证码文字的大小,默认为20sp
mTextSize = typedArray.getDimension(R.styleable.IndentifyingCode_textSize, typedArray.getDimensionPixelSize(R.styleable.IndentifyingCode_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics())));
// 一个好的习惯是用完资源要记得回收,就想打开数据库和IO流用完后要记得关闭一样
typedArray.recycle();

OK,自定义属性也完成了,值也获取到了,那么我们只需要将定制的属性值在我们onDraw()绘制的时候使用到就行了,自定义属性就是这么简单~,看到这里,也许有点混乱了,看一下完整代码整理一下。

package com.lt.identifyingcode;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.Random;
/**
* Created by lt on 2016/3/2.
*/
public class ValidationCode extends View{
/**
* 控件的宽度
*/
private int mWidth;
/**
* 控件的高度
*/
private int mHeight;
/**
* 验证码文本画笔
*/
private Paint mTextPaint; // 文本画笔
/**
* 干扰点坐标的集合
*/
private ArrayList mPoints = new ArrayList();
private Random mRandom = new Random();;
/**
* 干扰点画笔
*/
private Paint mPointPaint;
/**
* 绘制贝塞尔曲线的路径集合
*/
private ArrayList mPaths = new ArrayList();
/**
* 干扰线画笔
*/
private Paint mPathPaint;
/**
* 验证码字符串
*/
private String mCodeString;
/**
* 验证码的位数
*/
private int mCodeCount;
/**
* 验证码字符的大小
*/
private float mTextSize;
/**
* 验证码字符串的显示宽度
*/
private float mTextWidth;
/**
* 在java代码中创建view的时候调用,即new
* @param context
*/
public ValidationCode(Context context) {
this(context,null);
}
/**
* 在xml布局文件中使用view但没有指定style的时候调用
* @param context
* @param attrs
*/
public ValidationCode(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* 在xml布局文件中使用view并指定style的时候调用
* @param context
* @param attrs
* @param defStyleAttr
*/
public ValidationCode(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
getAttrValues(context, attrs);
// 做一些初始化工作
init();
}
/**
* 获取布局文件中的值
* @param context
*/
private void getAttrValues(Context context,AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndentifyingCode);
mCodeCount = typedArray.getInteger(R.styleable.IndentifyingCode_codeCount, 5); // 获取布局中验证码位数属性值,默认为5个
// 获取布局中验证码文字的大小,默认为20sp
mTextSize = typedArray.getDimension(R.styleable.IndentifyingCode_textSize, typedArray.getDimensionPixelSize(R.styleable.IndentifyingCode_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics())));
// 一个好的习惯是用完资源要记得回收,就想打开数据库和IO流用完后要记得关闭一样
typedArray.recycle();
}
/**
* 要像layout_width和layout_height属性支持wrap_content就必须重新这个方法
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 分别测量控件的宽度和高度,基本为模板方法
int measureWidth = measureWidth(widthMeasureSpec);
int measureHeight = measureHeight(heightMeasureSpec);
// 其实这个方法最终会调用setMeasuredDimension(int measureWidth,int measureHeight);
// 将测量出来的宽高设置进去完成测量
setMeasuredDimension(measureWidth, measureHeight);
}
@Override
protected void onDraw(Canvas canvas) {
// 初始化数据
initData();
int length = mCodeString.length();
float charLength = mTextWidth/length;
for(int i=1;i<=length;i++){
int offsetDegree = mRandom.nextInt(15);
// 这里只会产生0和1,如果是1那么正旋转正角度,否则旋转负角度
offsetDegree = mRandom.nextInt(2) == 1&#63;offsetDegree:-offsetDegree;
canvas.save();
canvas.rotate(offsetDegree, mWidth / 2, mHeight / 2);
// 给画笔设置随机颜色
mTextPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);
canvas.drawText(String.valueOf(mCodeString.charAt(i - 1)), (i-1) * charLength * 1.6f+30, mHeight * 2 / 3f, mTextPaint);
canvas.restore();
}
// 产生干扰效果1 -- 干扰点
for(PointF pointF : mPoints){
mPointPaint.setARGB(255,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20,mRandom.nextInt(200)+20);
canvas.drawPoint(pointF.x,pointF.y,mPointPaint);
}
// 产生干扰效果2 -- 干扰线
for(Path path : mPaths){
mPathPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);
canvas.drawPath(path, mPathPaint);
}
}
private void initData() {
// 获取控件的宽和高,此时已经测量完成
mHeight = getHeight();
mWidth = getWidth();
mPoints.clear();
// 生成干扰点坐标
for(int i=0;i<150;i++){
PointF pointF = new PointF(mRandom.nextInt(mWidth)+10,mRandom.nextInt(mHeight)+10);
mPoints.add(pointF);
}
mPaths.clear();
// 生成干扰线坐标
for(int i=0;i<2;i++){
Path path = new Path();
int startX = mRandom.nextInt(mWidth/3)+10;
int startY = mRandom.nextInt(mHeight/3)+10;
int endX = mRandom.nextInt(mWidth/2)+mWidth/2-10;
int endY = mRandom.nextInt(mHeight/2)+mHeight/2-10;
path.moveTo(startX,startY);
path.quadTo(Math.abs(endX-startX)/2,Math.abs(endY-startY)/2,endX,endY);
mPaths.add(path);
}
}
/**
* 初始化一些数据
*/
private void init() {
// 生成随机数字和字母组合
mCodeString = getCharAndNumr(mCodeCount);
// 初始化文字画笔
mTextPaint = new Paint();
mTextPaint.setStrokeWidth(3); // 画笔大小为3
mTextPaint.setTextSize(mTextSize); // 设置文字大小
// 初始化干扰点画笔
mPointPaint = new Paint();
mPointPaint.setStrokeWidth(6);
mPointPaint.setStrokeCap(Paint.Cap.ROUND); // 设置断点处为圆形
// 初始化干扰线画笔
mPathPaint = new Paint();
mPathPaint.setStrokeWidth(5);
mPathPaint.setColor(Color.GRAY);
mPathPaint.setStyle(Paint.Style.STROKE); // 设置画笔为空心
mPathPaint.setStrokeCap(Paint.Cap.ROUND); // 设置断点处为圆形
// 取得验证码字符串显示的宽度值
mTextWidth = mTextPaint.measureText(mCodeString);
}
/**
* java生成随机数字和字母组合
* @param length[生成随机数的长度]
* @return
*/
public static String getCharAndNumr(int length) {
String val = "";
Random random = new Random();
for (int i = 0; i 

总结:这里与其说自定义View到不如说是绘制图形,关键在于坐标点的计算,这里在计算坐标上也许不太好,以上是给大家分享Android自定义view制作绚丽的验证码,希望对大家有所帮助!大家有什么好的思路或者建议希望可以留言告诉我,感激不尽~。


推荐阅读
  • Android 九宫格布局详解及实现:人人网应用示例
    本文深入探讨了人人网Android应用中独特的九宫格布局设计,解析其背后的GridView实现原理,并提供详细的代码示例。这种布局方式不仅美观大方,而且在现代Android应用中较为少见,值得开发者借鉴。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 理解存储器的层次结构有助于程序员优化程序性能,通过合理安排数据在不同层级的存储位置,提升CPU的数据访问速度。本文详细探讨了静态随机访问存储器(SRAM)和动态随机访问存储器(DRAM)的工作原理及其应用场景,并介绍了存储器模块中的数据存取过程及局部性原理。 ... [详细]
  • Android LED 数字字体的应用与实现
    本文介绍了一种适用于 Android 应用的 LED 数字字体(digital font),并详细描述了其在 UI 设计中的应用场景及其实现方法。这种字体常用于视频、广告倒计时等场景,能够增强视觉效果。 ... [详细]
  • RecyclerView初步学习(一)
    RecyclerView初步学习(一)ReCyclerView提供了一种插件式的编程模式,除了提供ViewHolder缓存模式,还可以自定义动画,分割符,布局样式,相比于传统的ListVi ... [详细]
  • SQLite 动态创建多个表的需求在网络上有不少讨论,但很少有详细的解决方案。本文将介绍如何在 Qt 环境中使用 QString 类轻松实现 SQLite 表的动态创建,并提供详细的步骤和示例代码。 ... [详细]
  • 并发编程:深入理解设计原理与优化
    本文探讨了并发编程中的关键设计原则,特别是Java内存模型(JMM)的happens-before规则及其对多线程编程的影响。文章详细介绍了DCL双重检查锁定模式的问题及解决方案,并总结了不同处理器和内存模型之间的关系,旨在为程序员提供更深入的理解和最佳实践。 ... [详细]
  • 解决JAX-WS动态客户端工厂弃用问题并迁移到XFire
    在处理Java项目中的JAR包冲突时,我们遇到了JaxWsDynamicClientFactory被弃用的问题,并成功将其迁移到org.codehaus.xfire.client。本文详细介绍了这一过程及解决方案。 ... [详细]
  • 探讨如何真正掌握Java EE,包括所需技能、工具和实践经验。资深软件教学总监李刚分享了对毕业生简历中常见问题的看法,并提供了详尽的标准。 ... [详细]
  • 本文介绍如何使用布局文件在Android应用中排列多行TextView和Button,使其占据屏幕的特定比例,并提供示例代码以帮助理解和实现。 ... [详细]
  • TechStride 网站
    TechStride 成立于2014年初,致力于互联网前沿技术、产品创意及创业内容的聚合、搜索、学习与展示。我们旨在为互联网从业者提供更高效的新技术搜索、学习、分享和产品推广平台。 ... [详细]
  • 本文介绍了多个关于JavaScript的书籍资源、实用工具和编程实例,涵盖从入门到进阶的各个阶段,帮助读者全面提升JavaScript编程能力。 ... [详细]
  • 分享一个简化版的Silverlight链接图项目:Link Map Simplified
    本文介绍了一个使用Silverlight开发的可视化工具,主要用于展示和操作复杂的实体关系图(Graph)。该工具在犯罪调查系统中得到了广泛应用,帮助用户直观地获取和理解相关信息。 ... [详细]
  • 本文介绍了Android开发中Intent的基本概念及其在不同Activity之间的数据传递方式,详细展示了如何通过Intent实现Activity间的跳转和数据传输。 ... [详细]
  • 2020年悄然过半,时间的宝贵与无情令人深思。自去年12月开始撰写公众号以来,不知不觉已接近一年。本文将对findyi公众号在技术管理、认知提升、创业经验、职场发展、产品运营及个人成长等方面的文章进行总结,为读者提供一次回顾和补漏的机会。 ... [详细]
author-avatar
书友66421539
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有