Android中關(guān)于定時(shí)任務(wù)實(shí)現(xiàn)關(guān)閉訂單問(wèn)題
在電商、支付等領(lǐng)域,往往會(huì)有這樣的場(chǎng)景,用戶下單后放棄支付了,那這筆訂單會(huì)在指定的時(shí)間段后進(jìn)行關(guān)閉操作,細(xì)心的你一定發(fā)現(xiàn)了像某寶、某東都有這樣的邏輯,而且時(shí)間很準(zhǔn)確,誤差在1s內(nèi);那他們是怎么實(shí)現(xiàn)的呢?
一般的做法有如下幾種
- 定時(shí)任務(wù)關(guān)閉訂單
- rocketmq延遲隊(duì)列
- rabbitmq死信隊(duì)列
- 時(shí)間輪算法
- redis過(guò)期監(jiān)聽(tīng)
一、定時(shí)任務(wù)關(guān)閉訂單(最low)
一般情況下,最不推薦的方式就是關(guān)單方式就是定時(shí)任務(wù)方式,原因我們可以看下面的圖來(lái)說(shuō)明
我們假設(shè),關(guān)單時(shí)間為下單后10分鐘,定時(shí)任務(wù)間隔也是10分鐘;通過(guò)上圖我們看出,如果在第1分鐘下單,在第20分鐘的時(shí)候才能被掃描到執(zhí)行關(guān)單操作,這樣誤差達(dá)到10分鐘,這在很多場(chǎng)景下是不可接受的,另外需要頻繁掃描主訂單號(hào)造成網(wǎng)絡(luò)IO和磁盤(pán)IO的消耗,對(duì)實(shí)時(shí)交易造成一定的沖擊,所以PASS
二、rocketmq延遲隊(duì)列方式
延遲消息 生產(chǎn)者把消息發(fā)送到消息服務(wù)器后,并不希望被立即消費(fèi),而是等待指定時(shí)間后才可以被消費(fèi)者消費(fèi),這類(lèi)消息通常被稱(chēng)為延遲消息。 在RocketMQ開(kāi)源版本中,支持延遲消息,但是不支持任意時(shí)間精度的延遲消息,只支持特定級(jí)別的延遲消息。 消息延遲級(jí)別分別為1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,共18個(gè)級(jí)別。
發(fā)送延遲消息(生產(chǎn)者)
/** * 推送延遲消息 * @param topic * @param body * @param producerGroup * @return boolean */ public boolean sendMessage(String topic, String body, String producerGroup) { try { Message recordMsg = new Message(topic, body.getBytes()); producer.setProducerGroup(producerGroup); //設(shè)置消息延遲級(jí)別,我這里設(shè)置14,對(duì)應(yīng)就是延時(shí)10分鐘 // "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h" recordMsg.setDelayTimeLevel(14); // 發(fā)送消息到一個(gè)Broker SendResult sendResult = producer.send(recordMsg); // 通過(guò)sendResult返回消息是否成功送達(dá) log.info("發(fā)送延遲消息結(jié)果:======sendResult:{}", sendResult); DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); log.info("發(fā)送時(shí)間:{}", format.format(new Date())); return true; } catch (Exception e) { e.printStackTrace(); log.error("延遲消息隊(duì)列推送消息異常:{},推送內(nèi)容:{}", e.getMessage(), body); } return false; }
消費(fèi)延遲消息(消費(fèi)者)
/** * 接收延遲消息 * * @param topic * @param consumerGroup * @param messageHandler */ public void messageListener(String topic, String consumerGroup, MessageListenerConcurrently messageHandler) { ThreadPoolUtil.execute(() -> { try { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(); consumer.setConsumerGroup(consumerGroup); consumer.setVipChannelEnabled(false); consumer.setNamesrvAddr(address); //設(shè)置消費(fèi)者拉取消息的策略,*表示消費(fèi)該topic下的所有消息,也可以指定tag進(jìn)行消息過(guò)濾 consumer.subscribe(topic, "*"); //消費(fèi)者端啟動(dòng)消息監(jiān)聽(tīng),一旦生產(chǎn)者發(fā)送消息被監(jiān)聽(tīng)到,就打印消息,和rabbitmq中的handlerDelivery類(lèi)似 consumer.registerMessageListener(messageHandler); consumer.start(); log.info("啟動(dòng)延遲消息隊(duì)列監(jiān)聽(tīng)成功:" + topic); } catch (MQClientException e) { log.error("啟動(dòng)延遲消息隊(duì)列監(jiān)聽(tīng)失敗:{}", e.getErrorMessage()); System.exit(1); } }); }
實(shí)現(xiàn)監(jiān)聽(tīng)類(lèi),處理具體邏輯
/** * 延遲消息監(jiān)聽(tīng) * */ @Component public class CourseOrderTimeoutListener implements ApplicationListener<ApplicationReadyEvent> { @Resource private MQUtil mqUtil; @Resource private CourseOrderTimeoutHandler courseOrderTimeoutHandler; @Override public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { // 訂單超時(shí)監(jiān)聽(tīng) mqUtil.messageListener(EnumTopic.ORDER_TIMEOUT, EnumGroup.ORDER_TIMEOUT_GROUP, courseOrderTimeoutHandler); } }
/** * 實(shí)現(xiàn)監(jiān)聽(tīng) */ @Slf4j @Component public class CourseOrderTimeoutHandler implements MessageListenerConcurrently { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for (MessageExt msg : list) { // 得到消息體 String body = new String(msg.getBody()); JSONObject userJson = JSONObject.parseObject(body); TCourseBuy courseBuyDetails = JSON.toJavaObject(userJson, TCourseBuy.class); // 處理具體的業(yè)務(wù)邏輯,,,,, DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); log.info("消費(fèi)時(shí)間:{}", format.format(new Date())); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }
這種方式相比定時(shí)任務(wù)好了很多,但是有一個(gè)致命的缺點(diǎn),就是延遲等級(jí)只有18種(商業(yè)版本支持自定義時(shí)間),如果我們想把關(guān)閉訂單時(shí)間設(shè)置在15分鐘該如何處理呢?顯然不夠靈活。
三、rabbitmq死信隊(duì)列的方式
Rabbitmq本身是沒(méi)有延遲隊(duì)列的,只能通過(guò)Rabbitmq本身隊(duì)列的特性來(lái)實(shí)現(xiàn),想要Rabbitmq實(shí)現(xiàn)延遲隊(duì)列,需要使用Rabbitmq的死信交換機(jī)(Exchange)和消息的存活時(shí)間TTL(Time To Live)
死信交換機(jī) 一個(gè)消息在滿足如下條件下,會(huì)進(jìn)死信交換機(jī),記住這里是交換機(jī)而不是隊(duì)列,一個(gè)交換機(jī)可以對(duì)應(yīng)很多隊(duì)列。
一個(gè)消息被Consumer拒收了,并且reject方法的參數(shù)里requeue是false。也就是說(shuō)不會(huì)被再次放在隊(duì)列里,被其他消費(fèi)者使用。 上面的消息的TTL到了,消息過(guò)期了。
隊(duì)列的長(zhǎng)度限制滿了。排在前面的消息會(huì)被丟棄或者扔到死信路由上。 死信交換機(jī)就是普通的交換機(jī),只是因?yàn)槲覀儼堰^(guò)期的消息扔進(jìn)去,所以叫死信交換機(jī),并不是說(shuō)死信交換機(jī)是某種特定的交換機(jī)
消息TTL(消息存活時(shí)間) 消息的TTL就是消息的存活時(shí)間。RabbitMQ可以對(duì)隊(duì)列和消息分別設(shè)置TTL。對(duì)隊(duì)列設(shè)置就是隊(duì)列沒(méi)有消費(fèi)者連著的保留時(shí)間,也可以對(duì)每一個(gè)單獨(dú)的消息做單獨(dú)的設(shè)置。超過(guò)了這個(gè)時(shí)間,我們認(rèn)為這個(gè)消息就死了,稱(chēng)之為死信。如果隊(duì)列設(shè)置了,消息也設(shè)置了,那么會(huì)取值較小的。所以一個(gè)消息如果被路由到不同的隊(duì)列中,這個(gè)消息死亡的時(shí)間有可能不一樣(不同的隊(duì)列設(shè)置)。這里單講單個(gè)消息的TTL,因?yàn)樗攀菍?shí)現(xiàn)延遲任務(wù)的關(guān)鍵。
byte[] messageBodyBytes = "Hello, world!".getBytes(); AMQP.BasicProperties properties = new AMQP.BasicProperties(); properties.setExpiration("60000"); channel.basicPublish("my-exchange", "queue-key", properties, messageBodyBytes);
可以通過(guò)設(shè)置消息的expiration字段或者x-message-ttl屬性來(lái)設(shè)置時(shí)間,兩者是一樣的效果。只是expiration字段是字符串參數(shù),所以要寫(xiě)個(gè)int類(lèi)型的字符串:當(dāng)上面的消息扔到隊(duì)列中后,過(guò)了60秒,如果沒(méi)有被消費(fèi),它就死了。不會(huì)被消費(fèi)者消費(fèi)到。這個(gè)消息后面的,沒(méi)有“死掉”的消息對(duì)頂上來(lái),被消費(fèi)者消費(fèi)。死信在隊(duì)列中并不會(huì)被刪除和釋放,它會(huì)被統(tǒng)計(jì)到隊(duì)列的消息數(shù)中去
處理流程圖
創(chuàng)建交換機(jī)(Exchanges)和隊(duì)列(Queues)
創(chuàng)建死信交換機(jī)
如圖所示,就是創(chuàng)建一個(gè)普通的交換機(jī),這里為了方便區(qū)分,把交換機(jī)的名字取為:delay
創(chuàng)建自動(dòng)過(guò)期消息隊(duì)列 這個(gè)隊(duì)列的主要作用是讓消息定時(shí)過(guò)期的,比如我們需要2小時(shí)候關(guān)閉訂單,我們就需要把消息放進(jìn)這個(gè)隊(duì)列里面,把消息過(guò)期時(shí)間設(shè)置為2小時(shí)
創(chuàng)建一個(gè)一個(gè)名為delay_queue1的自動(dòng)過(guò)期的隊(duì)列,當(dāng)然圖片上面的參數(shù)并不會(huì)讓消息自動(dòng)過(guò)期,因?yàn)槲覀儾](méi)有設(shè)置x-message-ttl參數(shù),如果整個(gè)隊(duì)列的消息有消息都是相同的,可以設(shè)置,這里為了靈活,所以并沒(méi)有設(shè)置,另外兩個(gè)參數(shù)x-dead-letter-exchange代表消息過(guò)期后,消息要進(jìn)入的交換機(jī),這里配置的是delay,也就是死信交換機(jī),x-dead-letter-routing-key是配置消息過(guò)期后,進(jìn)入死信交換機(jī)的routing-key,跟發(fā)送消息的routing-key一個(gè)道理,根據(jù)這個(gè)key將消息放入不同的隊(duì)列
創(chuàng)建消息處理隊(duì)列 這個(gè)隊(duì)列才是真正處理消息的隊(duì)列,所有進(jìn)入這個(gè)隊(duì)列的消息都會(huì)被處理
消息隊(duì)列的名字為delay_queue2 消息隊(duì)列綁定到交換機(jī) 進(jìn)入交換機(jī)詳情頁(yè)面,將創(chuàng)建的2個(gè)隊(duì)列(delayqueue1和delayqueue2)綁定到交換機(jī)上面
自動(dòng)過(guò)期消息隊(duì)列的routing key 設(shè)置為delay 綁定delayqueue2
delayqueue2 的key要設(shè)置為創(chuàng)建自動(dòng)過(guò)期的隊(duì)列的x-dead-letter-routing-key參數(shù),這樣當(dāng)消息過(guò)期的時(shí)候就可以自動(dòng)把消息放入delay_queue2這個(gè)隊(duì)列中了 綁定后的管理頁(yè)面如下圖:
當(dāng)然這個(gè)綁定也可以使用代碼來(lái)實(shí)現(xiàn),只是為了直觀表現(xiàn),所以本文使用的管理平臺(tái)來(lái)操作 發(fā)送消息
String msg = "hello word"; MessageProperties messageProperties = newMessageProperties(); messageProperties.setExpiration("6000"); messageProperties.setCorrelationId(UUID.randomUUID().toString().getBytes()); Message message = newMessage(msg.getBytes(), messageProperties); rabbitTemplate.convertAndSend("delay", "delay",message);
設(shè)置了讓消息6秒后過(guò)期 注意:因?yàn)橐屜⒆詣?dòng)過(guò)期,所以一定不能設(shè)置delay_queue1的監(jiān)聽(tīng),不能讓這個(gè)隊(duì)列里面的消息被接受到,否則消息一旦被消費(fèi),就不存在過(guò)期了
接收消息 接收消息配置好delay_queue2的監(jiān)聽(tīng)就好了
package wang.raye.rabbitmq.demo1; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration publicclassDelayQueue{ /** 消息交換機(jī)的名字*/ publicstaticfinalString EXCHANGE = "delay"; /** 隊(duì)列key1*/ publicstaticfinalString ROUTINGKEY1 = "delay"; /** 隊(duì)列key2*/ publicstaticfinalString ROUTINGKEY2 = "delay_key"; /** * 配置鏈接信息 * @return */ @Bean publicConnectionFactory connectionFactory() { CachingConnectionFactory connectionFactory = newCachingConnectionFactory("120.76.237.8",5672); connectionFactory.setUsername("kberp"); connectionFactory.setPassword("kberp"); connectionFactory.setVirtualHost("/"); connectionFactory.setPublisherConfirms(true); // 必須要設(shè)置 return connectionFactory; } /** * 配置消息交換機(jī) * 針對(duì)消費(fèi)者配置 FanoutExchange: 將消息分發(fā)到所有的綁定隊(duì)列,無(wú)routingkey的概念 HeadersExchange :通過(guò)添加屬性key-value匹配 DirectExchange:按照routingkey分發(fā)到指定隊(duì)列 TopicExchange:多關(guān)鍵字匹配 */ @Bean publicDirectExchange defaultExchange() { returnnewDirectExchange(EXCHANGE, true, false); } /** * 配置消息隊(duì)列2 * 針對(duì)消費(fèi)者配置 * @return */ @Bean publicQueue queue() { returnnewQueue("delay_queue2", true); //隊(duì)列持久 } /** * 將消息隊(duì)列2與交換機(jī)綁定 * 針對(duì)消費(fèi)者配置 * @return */ @Bean @Autowired publicBinding binding() { returnBindingBuilder.bind(queue()).to(defaultExchange()).with(DelayQueue.ROUTINGKEY2); } /** * 接受消息的監(jiān)聽(tīng),這個(gè)監(jiān)聽(tīng)會(huì)接受消息隊(duì)列1的消息 * 針對(duì)消費(fèi)者配置 * @return */ @Bean @Autowired publicSimpleMessageListenerContainer messageContainer2(ConnectionFactory connectionFactory) { SimpleMessageListenerContainer container = newSimpleMessageListenerContainer(connectionFactory()); container.setQueues(queue()); container.setExposeListenerChannel(true); container.setMaxConcurrentConsumers(1); container.setConcurrentConsumers(1); container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //設(shè)置確認(rèn)模式手工確認(rèn) container.setMessageListener(newChannelAwareMessageListener() { publicvoid onMessage(Message message, com.rabbitmq.client.Channel channel) throwsException{ byte[] body = message.getBody(); System.out.println("delay_queue2 收到消息 : "+ newString(body)); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //確認(rèn)消息成功消費(fèi) } }); return container; } }
這種方式可以自定義進(jìn)入死信隊(duì)列的時(shí)間;是不是很完美,但是有的小伙伴的情況是消息中間件就是rocketmq,公司也不可能會(huì)用商業(yè)版,怎么辦?那就進(jìn)入下一節(jié)
四、時(shí)間輪算法
(1)創(chuàng)建環(huán)形隊(duì)列,例如可以創(chuàng)建一個(gè)包含3600個(gè)slot的環(huán)形隊(duì)列(本質(zhì)是個(gè)數(shù)組)
(2)任務(wù)集合,環(huán)上每一個(gè)slot是一個(gè)Set 同時(shí),啟動(dòng)一個(gè)timer,這個(gè)timer每隔1s,在上述環(huán)形隊(duì)列中移動(dòng)一格,有一個(gè)Current Index指針來(lái)標(biāo)識(shí)正在檢測(cè)的slot。
Task結(jié)構(gòu)中有兩個(gè)很重要的屬性: (1)Cycle-Num:當(dāng)Current Index第幾圈掃描到這個(gè)Slot時(shí),執(zhí)行任務(wù) (2)訂單號(hào),要關(guān)閉的訂單號(hào)(也可以是其他信息,比如:是一個(gè)基于某個(gè)訂單號(hào)的任務(wù))
假設(shè)當(dāng)前Current Index指向第0格,例如在3610秒之后,有一個(gè)訂單需要關(guān)閉,只需: (1)計(jì)算這個(gè)訂單應(yīng)該放在哪一個(gè)slot,當(dāng)我們計(jì)算的時(shí)候現(xiàn)在指向1,3610秒之后,應(yīng)該是第10格,所以這個(gè)Task應(yīng)該放在第10個(gè)slot的Set中 (2)計(jì)算這個(gè)Task的Cycle-Num,由于環(huán)形隊(duì)列是3600格(每秒移動(dòng)一格,正好1小時(shí)),這個(gè)任務(wù)是3610秒后執(zhí)行,所以應(yīng)該繞3610/3600=1圈之后再執(zhí)行,于是Cycle-Num=1
Current Index不停的移動(dòng),每秒移動(dòng)到一個(gè)新slot,這個(gè)slot中對(duì)應(yīng)的Set,每個(gè)Task看Cycle-Num是不是0: (1)如果不是0,說(shuō)明還需要多移動(dòng)幾圈,將Cycle-Num減1 (2)如果是0,說(shuō)明馬上要執(zhí)行這個(gè)關(guān)單Task了,取出訂單號(hào)執(zhí)行關(guān)單(可以用單獨(dú)的線程來(lái)執(zhí)行Task),并把這個(gè)訂單信息從Set中刪除即可。 (1)無(wú)需再輪詢(xún)?nèi)坑唵?,效率?(2)一個(gè)訂單,任務(wù)只執(zhí)行一次 (3)時(shí)效性好,精確到秒(控制timer移動(dòng)頻率可以控制精度)
五、redis過(guò)期監(jiān)聽(tīng)
1.修改redis.windows.conf配置文件中notify-keyspace-events的值 默認(rèn)配置notify-keyspace-events的值為 "" 修改為 notify-keyspace-events Ex 這樣便開(kāi)啟了過(guò)期事件
2. 創(chuàng)建配置類(lèi)RedisListenerConfig(配置RedisMessageListenerContainer這個(gè)Bean)
package com.zjt.shop.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisListenerConfig { @Autowired private RedisTemplate redisTemplate; /** * @return */ @Bean public RedisTemplate redisTemplateInit() { // key序列化 redisTemplate.setKeySerializer(new StringRedisSerializer()); //val實(shí)例化 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return redisTemplate; } @Bean RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); return container; } }
3.繼承KeyExpirationEventMessageListener創(chuàng)建redis過(guò)期事件的監(jiān)聽(tīng)類(lèi)
package com.zjt.shop.common.util; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.zjt.shop.modules.order.service.OrderInfoService; import com.zjt.shop.modules.product.entity.OrderInfoEntity; import com.zjt.shop.modules.product.mapper.OrderInfoMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.stereotype.Component; @Slf4j @Component public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener { public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } @Autowired private OrderInfoMapper orderInfoMapper; /** * 針對(duì)redis數(shù)據(jù)失效事件,進(jìn)行數(shù)據(jù)處理 * @param message * @param pattern */ @Override public void onMessage(Message message, byte[] pattern) { try { String key = message.toString(); //從失效key中篩選代表訂單失效的key if (key != null && key.startsWith("order_")) { //截取訂單號(hào),查詢(xún)訂單,如果是未支付狀態(tài)則為-取消訂單 String orderNo = key.substring(6); QueryWrapper<OrderInfoEntity> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("order_no",orderNo); OrderInfoEntity orderInfo = orderInfoMapper.selectOne(queryWrapper); if (orderInfo != null) { if (orderInfo.getOrderState() == 0) { //待支付 orderInfo.setOrderState(4); //已取消 orderInfoMapper.updateById(orderInfo); log.info("訂單號(hào)為【" + orderNo + "】超時(shí)未支付-自動(dòng)修改為已取消狀態(tài)"); } } } } catch (Exception e) { e.printStackTrace(); log.error("【修改支付訂單過(guò)期狀態(tài)異常】:" + e.getMessage()); } } }
4:測(cè)試 通過(guò)redis客戶端存一個(gè)有效時(shí)間為3s的訂單:
結(jié)果:
到此這篇關(guān)于Android中關(guān)于定時(shí)任務(wù)實(shí)現(xiàn)關(guān)閉訂單問(wèn)題的文章就介紹到這了,更多相關(guān)android 定時(shí)任務(wù)關(guān)閉訂單內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android編程之監(jiān)聽(tīng)器的實(shí)現(xiàn)方法
這篇文章主要介紹了Android編程之監(jiān)聽(tīng)器的實(shí)現(xiàn)方法,以實(shí)例形式較為詳細(xì)的分析了Android監(jiān)聽(tīng)器的創(chuàng)建、注冊(cè)及相關(guān)使用技巧,需要的朋友可以參考下2015-11-11Android編程使用緩存優(yōu)化ListView的方法
這篇文章主要介紹了Android編程使用緩存優(yōu)化ListView的方法,涉及ListView針對(duì)sd卡緩存及內(nèi)存緩存的優(yōu)化技巧,需要的朋友可以參考下2015-12-12Android拖拽助手ViewDragHelper的創(chuàng)建與使用實(shí)例
ViewDragHelper是針對(duì) ViewGroup 中的拖拽和重新定位 views 操作時(shí)提供了一系列非常有用的方法和狀態(tài)追蹤,下面這篇文章主要給大家介紹了關(guān)于Android拖拽助手ViewDragHelper的創(chuàng)建與使用的相關(guān)資料,需要的朋友可以參考下2022-05-05Android實(shí)現(xiàn)空心圓角矩形按鈕的實(shí)例代碼
頁(yè)面上有時(shí)會(huì)用到背景為空心圓角矩形的Button,可以通過(guò)xml繪制出來(lái)。這篇文章主要介紹了Android實(shí)現(xiàn)空心圓角矩形按鈕的實(shí)例代碼,需要的朋友參考下吧2017-01-01Android自定義View的使用及其原理知識(shí)點(diǎn)總結(jié)
在本篇文章里小編給大家整理的是關(guān)于Android自定義View的使用及其原理知識(shí)點(diǎn)總結(jié)內(nèi)容,需要的朋友們可以學(xué)習(xí)下。2019-08-08android實(shí)現(xiàn)http中請(qǐng)求訪問(wèn)添加cookie的方法
這篇文章主要介紹了android實(shí)現(xiàn)http中請(qǐng)求訪問(wèn)添加cookie的方法,實(shí)例分析了兩種添加cookie的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10Android入門(mén)之LinearLayout、AbsoluteLayout的用法實(shí)例講解
這篇文章主要介紹了Android入門(mén)之LinearLayout、AbsoluteLayout的用法,對(duì)于Android初學(xué)者有很好的參考借鑒價(jià)值,需要的朋友可以參考下2014-08-08Android簡(jiǎn)單實(shí)現(xiàn)菜單拖拽排序的功能
這篇文章主要介紹了Android簡(jiǎn)單實(shí)現(xiàn)菜單拖拽排序的功能,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)價(jià)值,需要的朋友可以參考一下2022-07-07