创建非正规形状按钮

预先警告:我发布这篇文章是因为代码很赞,并且清晰地阐明了Objective-C进行C语言底层位操作的能力。

如果你需要插入非正规形状的按钮,这篇文章就是为你量身定做的,请下载代码并阅读文章。如果你准备理解并学会使用这种方法,就应该仔细阅读代码。

下面是Jeff LaMarche博客中关于创建非正规形状按钮的文章:

http://iphonedevelopment.blogspot.com/2010/03/irregularly-shaped-uibuttons.html

以及:

http://iphonedevelopment.blogspot.com/2010/03/improved-irregular-shape-uibutton.html

上面文章的评论中有很多有用的建议,包括:

  • 使用calloc而非malloc,malloc无法将元素初始化到0,致使代码出现bug
  • 使用bitArray数组而非byteArray数组,从而降低内存的开销
  • 初始化边界检查,降低运算量

评论还指出了这份代码存在内存泄露。

我还上传了这个工程的第三版代码,功能如下:

  • 实现上述所有建议
  • 修正了内存泄露
  • 重写函数pointInside: withEvent: 而非 hitTest: withEvent: ( 见文档hitTest )
  • 删除了延迟加载,当UIButton对象被初始化或者图像被改变时进行初始化
  • 允许你设置一个alpha level的阈值,超过该阈值的按压无法记录

这部分工作涉及全部重建,包括在丢弃前为它创建内存等,而非通过底层例行程序抓取Alpha的数据。

我将所有底层操作打包到一个单独的例行程序,自动释放NSData对象。这个例行程序能够细心的释放它分配的所有内存。

我删除了延迟加载——对象第一次被按压时就进行精密计算的想法非常糟——这会导致令人恼怒的不一致的UI体验。通过使用bitArray数组,内存占用量减少了8倍,因此没有必要再去节省内存。你或许认为重写UIButton的setImage和setBackgroundImage函数就足够了。但是,如果它从NIB中创建,这些函数就不会被激活。一定要直接设置iVar

我发现通过设置突破点,每次按压时pointInside: withEvent会撞击三次。我即使检查调用栈,也无法理解出现这种情况的原因。但这加强了运行最少进程以便测试是否记录一次撞击的重要性。

总而言之,这个工程是内存优化管理的不错的小练习,并且展现了Objective-C驾驭C的能力。如果你仔细阅读代码,就会发现它也揭示了位图和位图环境的本质。

// //  TestViewController.h //  测试 // //  Pi  
@interface TestViewController : UIViewController { }  
- (IBAction) buttonClick : (id) sender;  
@end
  // //  TestViewController.m //  Test // //  Pi   #import "TestViewController.h"  
@implementation TestViewController
  - (IBAction) buttonClick : (id) sender
{ NSLog(@"Clicked:%@", sender); }  
- (void) dealloc
{ [super dealloc]; }  
@end
  // //  ClickThruButton.h
//  button的定制 // //  Pi  
@class AlphaMask;  
@interface clickThruButton : UIButton
{ @private AlphaMask* _alphaMask; }  
@end
  // //  ClickThruButton.m //  Test // //  Pi   #import "clickThruButton.h" #import "AlphaMask.h"  
@interface clickThruButton ()
 
@property (nonatomic, retain) AlphaMask* alphaMask;  
- (void) myInit;
- (void) setMask;
  
@end
 
@implementation clickThruButton
 
@synthesize alphaMask = _alphaMask;   /*
 为了让这个对象具有通用性, 我们应该考虑到
 它从IB中调用或者直接从代码中调用的可能性.
 通过重写这些函数, 我们就能确保无论
 它如何被创建, 我们的初始化程序都能被调用.
 */ #pragma mark init // 
如果irregButtons从NIB创建
- (void)awakeFromNib
{ [super awakeFromNib]; [self myInit]; }  
// 如果irregButtons从代码创建或修改
- (id) initWithFrame: (CGRect) aRect
{ self = [super initWithFrame: aRect];
 if (self) [self myInit];
 return self;
}
   - (void) myInit
{
 // 经过设置,任何大于0x00的Alpha值(透明的)都不会产生点击
uint8_t threshold = 0x00;
self.alphaMask = [[AlphaMask alloc] initWithThreshold: threshold];
 [self setMask];
}
  #pragma mark if image changes
(void) setBackgroundImage: (UIImage *) _image
                   forState: (UIControlState) _state
{ [super setBackgroundImage: _image
                     forState: _state];
[self setMask];
 }
  - (void) setImage: (UIImage *) _image
         forState: (UIControlState) _state
{ [super setImage: _image
           forState: _state]; [self setMask]; }   #pragma mark Set alphaMask /*
 请注意我们从初始化函数
 和经过重写的图像的setter函数重新定向到此处
 
 我们不能仅仅重写setters函数 -- 如果对象从NIB加载,
 这些函数就不会被激活. 很明显必须直接设置iVars.
 
 按钮的图像每次发生改变,都应该调用这个函数
 因为它需要以一种快速接入碰撞检测器的
 方法提取、处理和压缩Alpha数据.
 */ -(void) setMask
{ UIImage *btnImage = [self imageForState: UIControlStateNormal];  
// 如果没有发现图像,就使用背景图
  if (btnImage == nil)
  btnImage = [self backgroundImageForState: UIControlStateNormal];
   if (btnImage == nil)
 { self.alphaMask = nil; return ; }  
     [self.alphaMask feedImage: btnImage.CGImage]; }
   #pragma mark Hit Test! /* 重写pointInside:withEvent:
 注意到我们没有直接重写hitTest. 如果你看文档
 就会发现这个按钮的PARENT's碰撞检测器
 将检查它的一个子女的pointInside函数
 */ - (BOOL) pointInside : (CGPoint) p
           withEvent : (UIEvent *) event
{ // 优化检查 -- 碰撞检测
 if (!CGRectContainsPoint(self.bounds, p))
 return NO;
  // 检查alphaMask的预计算的位数组, // 以确定检查点是否允许产生一个碰撞
 bool ret = [self.alphaMask hitTest: p];
   // 如果为是, 将“是”发送到双亲碰撞检测器, // 此检测器比调用栈高一级.
 // 因此在这个例子中, 双亲将成为view, // 并且检查它的所有子女 // 直到发现一个回答为“yes”的子女
return ret; }  
#pragma mark dealloc
- (void)dealloc
{ [self.alphaMask release]; [super dealloc]; }
@end
  // //  imageHelper.h //  test // //  Pi  
@interface  AlphaMask : NSObject
{
@private uint8_t alphaThreshold;
     size_t imageWidth;
     NSData* _bitArray; }
  - (id) initWithThreshold: (uint8_t) t;
  - (void) feedImage: (CGImageRef) img;
  - (bool) hitTest: (CGPoint) p;  
// Private methods and properties defined in the .m  
@end
  // //  imageHelper.m //  test // //  Pi   #import "AlphaMask.h"   // 私有函数的讨论如下: // http://stackoverflow.com/questions/172598/ //best-way-to-define-private-methods-for-a-class-in-objective-c //Objective-C并不直接支持私有函数. 使用一个 // 空的范畴是获得同样效果的可接受的黑客方式. @interface  AlphaMask ()
 // <-- empty category  
// 每个位代表一个像素: 为点进保持一个Yes/No的值, 1代表碰撞 0代表点进
@property (nonatomic, retain) NSData* bitArray;
  // 注意加号意味着STATIC函数
+ (NSData *) calcHitGridFromCGImage: (CGImageRef) img
           alphaThreshold: (uint8_t) alphaThreshold_ ;
@end
 
@implementation AlphaMask
 
@synthesize bitArray = _bitArray;
  #pragma mark Init stuff
/*
 下面有alphaThreshold的详细讨论
 简单地说如果你将它设置为0, 碰撞
 检测器仅仅通过100%透明的像素
 
 将其设置为64将通过所有
 透明度低于25%的像素
 
 255是最大值.此时,图像无法发生碰撞
 所有像素都将被通过.
 */
- (id) initWithThreshold: (uint8_t) alphaThreshold_
{ self = [super init];
 if (!self) return nil;  
    alphaThreshold = alphaThreshold_;
    self.bitArray = nil; imageWidth = 0;
   return [self init]; }
  - (void) feedImage: (CGImageRef) img
{ self.bitArray = [AlphaMask calcHitGridFromCGImage: img
                        alphaThreshold: alphaThreshold];  
    imageWidth = CGImageGetWidth(img);
}   #pragma mark Hit Test! /*
 通过查找跟这个像素有关的bit数组中的位,
 确定像素是否发生碰撞(位的值是1)或通过点击(位的值是0)
 为了尽量减少开销, 我直接使用C的指针.
 
 注意: 出于某些原因, iOS每处理一个对象就有三次
 碰撞检测 -- 这一点很奇怪, 因此我们有理由
 尽可能在这个函数中花费尽量少的时间.
 */ - (bool) hitTest: (CGPoint) p
{ const uint8_t c_0x01 = 0x01;
   if (!self.bitArray) return NO;
  // 第一个字节的位置
uint8_t * pBitArray = (uint8_t *)
[self.bitArray bytes];  
// 第n个像素在第n个字节中(一个字节涵盖8个像素)
 size_t N = p.y * imageWidth + p.x;
size_t n = N / (size_t) 8;
uint8_t thisPixel = *(pBitArray + n) ;
  // 将我们想要的位作为掩码
 uint8_t mask = c_0x01 << (N % 8);
 // nonzero => Yes absorb HIT, zero => No - click-thru
return (thisPixel & mask) ? YES : NO;
}  
#pragma mark Extract alphaMask from image!
 // 构造一个压缩的位图(每像素占用一个位)存储每个像素
// 而无论这个像素是碰撞,还是通过点击
 // 如果像素的alpha值为零, 像素是透明的
// 如果像素的alpha值大于alphaThreshold的阈值, 相应的位设置为1,
// 表明这个像素获得碰撞
// 注意到将alphaThreshold阈值设置为0
 // 任何像素都是100%透明的,并将获得碰撞
+ (NSData *) calcHitGridFromCGImage: (CGImageRef) img
                     alphaThreshold: (uint8_t) alphaThreshold_
{ CGContextRef    alphaContext = NULL;
 void * alphaGrid;
   size_t w = CGImageGetWidth(img);
 size_t h = CGImageGetHeight(img);
   size_t bytesCount = w * h * sizeof(uint8_t);
   // 为alpha-only环境分配AND ZERO (所以不能使用malloc)内存
alphaGrid = calloc (bytesCount, sizeof(uint8_t));
if (alphaGrid == NULL)
{ fprintf (stderr, "calloc failed!"); return nil; }
  // 创建alpha-only环境
alphaContext = CGBitmapContextCreate (alphaGrid, w, h, 8,   w, NULL, kCGImageAlphaOnly);
if (alphaContext == NULL)
 { free (alphaGrid);
 fprintf (stderr, "Context not created!"); return nil; }
  // blat image onto alpha-only context
CGRect rect = {{0,0},{w,h}};
CGContextDrawImage(alphaContext, rect, img);
   // grab alpha-only image-data
void* _alphaData = CGBitmapContextGetData (alphaContext);
if (!_alphaData)
{ CGContextRelease(alphaContext);
free (alphaGrid); return nil;
}
uint8_t *alphaData = (uint8_t *) _alphaData;  
// --------------------------- // 压缩至每像素一个位 // ---------------------------  
size_t srcBytes = bytesCount;
size_t destBytes = srcBytes / (size_t) 8;
 if (srcBytes % 8) destBytes++;
  // 当我们清空每个目标字节时,可以使用malloc
 uint8_t* dest = malloc (destBytes);
 if (!dest) { CGContextRelease(alphaContext);
free (alphaGrid);
fprintf (stderr, "malloc failed!"); return nil; }
  size_t iDestByte = 0;
uint8_t target = 0x00, iBit = 0, c_0x01 = 0x01;
  for (size_t i=0; i < srcBytes; i++)
{ uint8_t src = *(alphaData++);
// 将获得碰撞的位设置为1,将通过点击的位设置为0 
 // alpha 0x00 是透明的 
// 如果不使用UNSIGNED数据类型,比较将失败 
 if (src > alphaThreshold_)
target |= (c_0x01 << iBit);
iBit++;
 if (iBit > 7)
{ dest[iDestByte] = target;
target = 0x00;  
            iDestByte++; iBit = 0; }
}   // COPIES buffer // is AUTORELEASED! // http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/ // MemoryMgmt/Articles/mmRules.html#//apple_ref/doc/uid/20000994-BAJHFBGH
NSData* ret = [NSData dataWithBytes: (const void *) dest
                                 length: (NSUInteger) destBytes ];
 
    CGContextRelease (alphaContext); free (alphaGrid); free (dest);  
return ret; }  
@end

原文链接:http://www.codeproject.com/KB/iPhone/IrregularlyShapedButtons.aspx

原作者:Ohmu

原文:

Download clickThruButton1 – 133.64 KB

Forewarning: I am putting this up because it is good code, and a neat illustration of Objective-C’s ability to descend into the depths of C some low-level bit-bashing.

If you need to implement irregularly shaped buttons, this is for you. Download the code, and the article will help you understand it. If you want to learn from it, you will need to pick through the code, and only then will my preamble be of use to you.

I followed with interest Jeff LaMarche’s blog posts on creating irregularly shaped buttons:

http://iphonedevelopment.blogspot.com/2010/03/irregularly-shaped-uibuttons.html

and the follow up:

http://iphonedevelopment.blogspot.com/2010/03/improved-irregular-shape-uibutton.html

The comments in the follow-up contain some good suggestions:

use of calloc instead of malloc — the code as is buggy, as malloc fails to initialize elements to 0
use of a bitArray instead of a byteArray to reduce memory usage
and initial bounding box check, to reduce computation
The comments also point out that the code leaks memory.

I have put together a third generation to this project, which:

implements all of the above suggestions
fixes the memory leaks
overrides pointInside : withEvent : rather than hitTest : withEvent : (see documentation for hitTest)
removes the lazy load, instead performing initialization when the UIButton object is initialized, or when its image is changed
allows you to set a threshold alpha level, beyond which presses will not register
This work involved a complete restructuring — rather than have one low-level routine grab the Alpha data, creating memory for it, before throwing it to another one, etc., I have packed all of the low-level stuff into a single routine that emits an auto released NSData object. This routine is careful to free all memory it allocates.

I took out the lazy load — it is a bad idea to perform intensive calculation the first time an object is pressed — this gives an inconsistent UI experience, which is exasperating. Also, by using a bitArray, the memory footprint is reduced by eight times, so there is less point in trying to save memory. You might think it would be enough to override the setImage and setBackgroundImage methods of UIButton. However, if it is created from a NIB, these methods do not get invoked. It must be that the iVar gets set directly.

I noticed by setting breakpoints that pointInside : withEvent : gets hit three times for each press. This is a mystery to me — even looking at the call stack doesn’t switch on the light bulb. However, it reinforces the importance of performing minimal processing to test whether the point will register a hit.

Overall, this project is a nice little exercise in memory management and optimization, and shows off the ability of Objective-C to harness the power of C. If you pick through the code, you can see it also gives some insight as to the nature of bitmaps and bitmap contexts.

/ //  TestViewController.h //  Test // //  Pi  
@interface TestViewController : UIViewController { }   - (IBAction) buttonClick : (id) sender;  
@end
  // //  TestViewController.m //  Test // //  Pi   #import "TestViewController.h"  
@implementation TestViewController
  - (IBAction) buttonClick : (id) sender { NSLog(@"Clicked:%@", sender); }   - (void) dealloc { [super dealloc]; }  
@end
  // //  ClickThruButton.h //  Test // //  Pi  
@class AlphaMask;  
@interface clickThruButton : UIButton { @private AlphaMask* _alphaMask; }  
@end
  // //  ClickThruButton.m //  Test // //  Pi   #import "clickThruButton.h" #import "AlphaMask.h"  
@interface clickThruButton ()  
@property (nonatomic, retain) AlphaMask* alphaMask;   - (void) myInit; - (void) setMask;  
@end
 
@implementation clickThruButton
 
@synthesize alphaMask = _alphaMask;   /*
 To make this object versatile, we should allow for the possibility
 that it is being used from IB, or directly from code.
 By overriding both these functions, we can ensure that
 however it is created, our custom initializer gets called.
 */ #pragma mark init // if irregButtons created from NIB - (void)awakeFromNib { [super awakeFromNib]; [self myInit]; }   // if irregButtons created or modified from code... - (id) initWithFrame: (CGRect) aRect { self = [super initWithFrame: aRect]; if (self) [self myInit]; return self; }   - (void) myInit { // Set so that any alpha > 0x00 (transparent) sinks the click uint8_t threshold = 0x00; self.alphaMask = [[AlphaMask alloc] initWithThreshold: threshold]; [self setMask]; }   #pragma mark if image changes... - (void) setBackgroundImage: (UIImage *) _image
                   forState: (UIControlState) _state { [super setBackgroundImage: _image
                     forState: _state]; [self setMask]; }   - (void) setImage: (UIImage *) _image
         forState: (UIControlState) _state { [super setImage: _image
           forState: _state]; [self setMask]; }   #pragma mark Set alphaMask /*
 Note that we get redirected here from both our custom initializer
 and the image setter methods which we have overridden.
 
 We can't just override the setters -- if the object is loading from a
 NIB these methods don't fire. Clearly it must set the iVars directly.
 
 This method should get invoked every time the buttons image changes.
 Because it needs to extract, process and compress the Alpha data,
 in a way that our hit tester can access quickly.
 */ -(void) setMask { UIImage *btnImage = [self imageForState: UIControlStateNormal];   // If no image found, try for background image if (btnImage == nil) btnImage = [self backgroundImageForState: UIControlStateNormal];   if (btnImage == nil) { self.alphaMask = nil; return ; }   [self.alphaMask feedImage: btnImage.CGImage]; }   #pragma mark Hit Test! /* override pointInside:withEvent:
 Notice that we don't directly override hitTest. If you look at the
 documentation you will see that this button's PARENT's hit tester
 will check the pointInside methods of one of its children.
 */ - (BOOL) pointInside : (CGPoint) p
           withEvent : (UIEvent *) event { // Optimization check -- bounding box if (!CGRectContainsPoint(self.bounds, p)) return NO;   // Checks the point against alphaMask's precalculated bit array, // to determine whether this point is allowed to register a hit bool ret = [self.alphaMask hitTest: p];   // If yes, send ' yes ' back to the parents hit tester, // which will be one level up the call stack. // So in this example, the parent will be the view, // and it will check through all of its children until // it finds one that responds with ' yes ' return ret; }   #pragma mark dealloc - (void)dealloc { [self.alphaMask release]; [super dealloc]; } @end
  // //  imageHelper.h //  test // //  Pi  
@interface  AlphaMask : NSObject { @private uint8_t alphaThreshold; size_t imageWidth; NSData* _bitArray; }   - (id) initWithThreshold: (uint8_t) t;   - (void) feedImage: (CGImageRef) img;   - (bool) hitTest: (CGPoint) p;   // Private methods and properties defined in the .m  
@end
  // //  imageHelper.m //  test // //  Pi   #import "AlphaMask.h"   // Private methods discussion here: // http://stackoverflow.com/questions/172598/ //best-way-to-define-private-methods-for-a-class-in-objective-c //Objective-C doesn't directly support private methods. Using an // empty category is an acceptably hacky way to achieve this effect. @interface  AlphaMask () // <-- empty category   // each bit represents 1 pixel: will hold Yes/No for click-thru, 1=hit 0=click-thru @property (nonatomic, retain) NSData* bitArray;   // note + means STATIC method + (NSData *) calcHitGridFromCGImage: (CGImageRef) img
           alphaThreshold: (uint8_t) alphaThreshold_ ; @end
 
@implementation AlphaMask
 
@synthesize bitArray = _bitArray;   #pragma mark Init stuff /*
 See below for a more detailed discussion on alphaThreshold.
 Basically if you set it to 0, the hit tester will only
 pass through pixels that are 100% transparent
 
 Setting it to 64 would pass through all pixels
 that are less than 25% transparent
 
 255 is the maximum. Setting to this, the image cannot
 take a hit -- everything passes through.
 */ - (id) initWithThreshold: (uint8_t) alphaThreshold_ { self = [super init]; if (!self) return nil;  
    alphaThreshold = alphaThreshold_; self.bitArray = nil; imageWidth = 0;   return [self init]; }   - (void) feedImage: (CGImageRef) img { self.bitArray = [AlphaMask calcHitGridFromCGImage: img
                        alphaThreshold: alphaThreshold];  
    imageWidth = CGImageGetWidth(img); }   #pragma mark Hit Test! /*
 Ascertains, through looking up the relevant bit in our bit array
 that pertains to this pixel, whether the pixel should take the hit
 (bit set to 1) or allow the click to pass through (bit set to 0).
 In order to minimize overhead, I am playing with C pointers directly.
 
 Note: for some reason, iOS seems to be hit testing each object
 three times -- which is bizarre, and another good reason for
 spending as little time as possible inside this function.
 */ - (bool) hitTest: (CGPoint) p { const uint8_t c_0x01 = 0x01;   if (!self.bitArray) return NO;   // location of first byte uint8_t * pBitArray = (uint8_t *) [self.bitArray bytes];   // the N'th pixel will lie in the n'th byte (one byte covers 8 pixels) size_t N = p.y * imageWidth + p.x; size_t n = N / (size_t) 8; uint8_t thisPixel = *(pBitArray + n) ;   // mask with the bit we want uint8_t mask = c_0x01 << (N % 8); // nonzero => Yes absorb HIT, zero => No - click-thru return (thisPixel & mask) ? YES : NO; }   #pragma mark Extract alphaMask from image! // Constructs a compressed bitmap (one bit per pixel) that stores for each pixel //     whether that pixel should accept the hit, or pass it through. // If the pixels alpha value is zero, the pixel is transparent // if the pixels alpha value > alphaThreshold, the corresponding bit is set to 1, //     indicating that this pixel is to receive a hit //Note that setting alphaThreshold to 0 means that any pixel that is not //     100% transparent will receive a hit + (NSData *) calcHitGridFromCGImage: (CGImageRef) img
                     alphaThreshold: (uint8_t) alphaThreshold_ { CGContextRef    alphaContext = NULL; void * alphaGrid;   size_t w = CGImageGetWidth(img); size_t h = CGImageGetHeight(img);   size_t bytesCount = w * h * sizeof(uint8_t);   // allocate AND ZERO (so can't use malloc) memory for alpha-only context alphaGrid = calloc (bytesCount, sizeof(uint8_t)); if (alphaGrid == NULL) { fprintf (stderr, "calloc failed!"); return nil; }   // create alpha-only context alphaContext = CGBitmapContextCreate (alphaGrid, w, h, 8,   w, NULL, kCGImageAlphaOnly); if (alphaContext == NULL) { free (alphaGrid); fprintf (stderr, "Context not created!"); return nil; }   // blat image onto alpha-only context CGRect rect = {{0,0},{w,h}}; CGContextDrawImage(alphaContext, rect, img);   // grab alpha-only image-data void* _alphaData = CGBitmapContextGetData (alphaContext); if (!_alphaData) { CGContextRelease(alphaContext); free (alphaGrid); return nil; } uint8_t *alphaData = (uint8_t *) _alphaData;   // --------------------------- // compress to 1 bit per pixel // ---------------------------   size_t srcBytes = bytesCount; size_t destBytes = srcBytes / (size_t) 8; if (srcBytes % 8) destBytes++;   // malloc ok here, as we zero each target byte uint8_t* dest = malloc (destBytes); if (!dest) { CGContextRelease(alphaContext); free (alphaGrid); fprintf (stderr, "malloc failed!"); return nil; }   size_t iDestByte = 0; uint8_t target = 0x00, iBit = 0, c_0x01 = 0x01;   for (size_t i=0; i < srcBytes; i++) { uint8_t src = *(alphaData++); // set bit to 1 for 'takes hit', leave on 0 for 'click-thru'         // alpha 0x00 is transparent         // comparison fails famously if not using UNSIGNED data type         if (src > alphaThreshold_) target |= (c_0x01 << iBit); iBit++; if (iBit > 7) { dest[iDestByte] = target; target = 0x00;  
            iDestByte++; iBit = 0; } }   // COPIES buffer // is AUTORELEASED! // http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/ // MemoryMgmt/Articles/mmRules.html#//apple_ref/doc/uid/20000994-BAJHFBGH NSData* ret = [NSData dataWithBytes: (const void *) dest
                                 length: (NSUInteger) destBytes ];  
    CGContextRelease (alphaContext); free (alphaGrid); free (dest);   return ret; }  
@end

转载于:https://my.oschina.net/makeffort/blog/86807

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值