@ -1 +0,0 @@ |
|||||||
/build |
|
@ -1,38 +0,0 @@ |
|||||||
apply plugin: 'com.android.library' |
|
||||||
apply plugin: 'kotlin-android' |
|
||||||
apply plugin: 'kotlin-android-extensions' |
|
||||||
|
|
||||||
android { |
|
||||||
compileSdkVersion 29 |
|
||||||
buildToolsVersion "29.0.3" |
|
||||||
|
|
||||||
defaultConfig { |
|
||||||
minSdkVersion 16 |
|
||||||
targetSdkVersion 29 |
|
||||||
versionCode 5 |
|
||||||
versionName "1.3.5" |
|
||||||
|
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" |
|
||||||
consumerProguardFiles 'consumer-rules.pro' |
|
||||||
} |
|
||||||
|
|
||||||
buildTypes { |
|
||||||
release { |
|
||||||
minifyEnabled false |
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
dependencies { |
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar']) |
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" |
|
||||||
//noinspection GradleCompatible |
|
||||||
implementation 'com.android.support:appcompat-v7:28.0.0' |
|
||||||
testImplementation 'junit:junit:4.13' |
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2' |
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' |
|
||||||
implementation 'org.tensorflow:tensorflow-lite:2.1.0' |
|
||||||
implementation 'org.tensorflow:tensorflow-lite-gpu:2.1.0' |
|
||||||
} |
|
@ -1,21 +0,0 @@ |
|||||||
# Add project specific ProGuard rules here. |
|
||||||
# You can control the set of applied configuration files using the |
|
||||||
# proguardFiles setting in build.gradle. |
|
||||||
# |
|
||||||
# For more details, see |
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html |
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following |
|
||||||
# and specify the fully qualified class name to the JavaScript interface |
|
||||||
# class: |
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { |
|
||||||
# public *; |
|
||||||
#} |
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for |
|
||||||
# debugging stack traces. |
|
||||||
#-keepattributes SourceFile,LineNumberTable |
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to |
|
||||||
# hide the original source file name. |
|
||||||
#-renamesourcefileattribute SourceFile |
|
@ -1,22 +0,0 @@ |
|||||||
package com.zwy.opennsfw |
|
||||||
|
|
||||||
import android.support.test.InstrumentationRegistry |
|
||||||
import android.support.test.runner.AndroidJUnit4 |
|
||||||
import org.junit.Assert.assertEquals |
|
||||||
import org.junit.Test |
|
||||||
import org.junit.runner.RunWith |
|
||||||
|
|
||||||
/** |
|
||||||
* Instrumented test, which will execute on an Android device. |
|
||||||
* |
|
||||||
* See [testing documentation](http://d.android.com/tools/testing). |
|
||||||
*/ |
|
||||||
@RunWith(AndroidJUnit4::class) |
|
||||||
class ExampleInstrumentedTest { |
|
||||||
@Test |
|
||||||
fun useAppContext() { |
|
||||||
// Context of the app under test. |
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext |
|
||||||
assertEquals("com.zwy.opennsfw.test", appContext.packageName) |
|
||||||
} |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
<manifest package="com.zwy.opennsfw" /> |
|
@ -1,228 +0,0 @@ |
|||||||
package com.zwy.opennsfw.core |
|
||||||
|
|
||||||
import NsfwBean |
|
||||||
import android.content.Context |
|
||||||
import android.graphics.Bitmap |
|
||||||
import android.graphics.Color |
|
||||||
import android.os.SystemClock |
|
||||||
import d |
|
||||||
import e |
|
||||||
import mClassifier |
|
||||||
import org.tensorflow.lite.Interpreter |
|
||||||
import org.tensorflow.lite.gpu.GpuDelegate |
|
||||||
import java.io.ByteArrayOutputStream |
|
||||||
import java.io.File |
|
||||||
import java.io.FileInputStream |
|
||||||
import java.lang.Math.max |
|
||||||
import java.nio.ByteBuffer |
|
||||||
import java.nio.ByteOrder |
|
||||||
import java.nio.MappedByteBuffer |
|
||||||
import java.nio.channels.FileChannel |
|
||||||
|
|
||||||
|
|
||||||
class Classifier private constructor(config: Config) { |
|
||||||
|
|
||||||
/** |
|
||||||
* DIM_BATCH_SIZE |
|
||||||
*/ |
|
||||||
private val DIM_BATCH_SIZE = 1 |
|
||||||
|
|
||||||
/** |
|
||||||
* DIM_PIXEL_SIZE |
|
||||||
*/ |
|
||||||
private val DIM_PIXEL_SIZE = 3 |
|
||||||
|
|
||||||
/** |
|
||||||
* 数据宽高 |
|
||||||
*/ |
|
||||||
private val INPUT_WIDTH = 224 |
|
||||||
|
|
||||||
/** |
|
||||||
* 数据宽高 |
|
||||||
*/ |
|
||||||
private val INPUT_HEIGHT = 224 |
|
||||||
|
|
||||||
/** |
|
||||||
* 通道 |
|
||||||
*/ |
|
||||||
private val BYTES_PER_CHANNEL_NUM = 4 |
|
||||||
|
|
||||||
/** |
|
||||||
* Resize后的数据源 |
|
||||||
*/ |
|
||||||
private val intValues = IntArray(INPUT_WIDTH * INPUT_HEIGHT) |
|
||||||
|
|
||||||
|
|
||||||
/** |
|
||||||
* Tensorflow Lite |
|
||||||
*/ |
|
||||||
private var tflite: Interpreter |
|
||||||
|
|
||||||
/** |
|
||||||
* 喂入模型的最终数据源 |
|
||||||
*/ |
|
||||||
private var imgData: ByteBuffer |
|
||||||
|
|
||||||
companion object { |
|
||||||
private var config = Config(context = null) |
|
||||||
private var instance: Classifier? = null |
|
||||||
get() { |
|
||||||
if (field == null) { |
|
||||||
if (config.context == null) throw RuntimeException("context函数未调用,请使用Classifier.Build().context(context)初始化") |
|
||||||
field = Classifier(config) |
|
||||||
mClassifier = field |
|
||||||
} |
|
||||||
return field |
|
||||||
} |
|
||||||
|
|
||||||
@Synchronized |
|
||||||
private fun get(config: Config): Classifier { |
|
||||||
this.config = config |
|
||||||
return instance!! |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
class Build { |
|
||||||
private val config = Config(context = null) |
|
||||||
|
|
||||||
fun isOpenGPU(isOpenGPU: Boolean): Build { |
|
||||||
config.isOpenGPU = isOpenGPU |
|
||||||
return this |
|
||||||
} |
|
||||||
|
|
||||||
fun numThreads(numThreads: Int): Build { |
|
||||||
config.numThreads = numThreads |
|
||||||
return this |
|
||||||
} |
|
||||||
|
|
||||||
fun context(context: Context): Build { |
|
||||||
config.context = context |
|
||||||
return this |
|
||||||
} |
|
||||||
|
|
||||||
fun nsfwModuleFilePath(nsfwModuleFilePath: String): Build { |
|
||||||
config.nsfwModuleFilePath = nsfwModuleFilePath |
|
||||||
return this |
|
||||||
} |
|
||||||
|
|
||||||
fun build(): Classifier { |
|
||||||
return get(config) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
init { |
|
||||||
try { |
|
||||||
tflite = |
|
||||||
Interpreter( |
|
||||||
loadModelFile(context = config.context!!), |
|
||||||
getTfLiteOptions(config.isOpenGPU) |
|
||||||
) |
|
||||||
if (config.isOpenGPU) "开启GPU加速成功".d() |
|
||||||
} catch (e: Exception) { |
|
||||||
"不支持GPU加速".e() |
|
||||||
tflite = Interpreter(loadModelFile(context = config.context!!), getTfLiteOptions(false)) |
|
||||||
} |
|
||||||
|
|
||||||
imgData = ByteBuffer.allocateDirect( |
|
||||||
DIM_BATCH_SIZE |
|
||||||
* INPUT_WIDTH |
|
||||||
* INPUT_HEIGHT |
|
||||||
* DIM_PIXEL_SIZE |
|
||||||
* BYTES_PER_CHANNEL_NUM |
|
||||||
) |
|
||||||
|
|
||||||
imgData.order(ByteOrder.LITTLE_ENDIAN) |
|
||||||
} |
|
||||||
|
|
||||||
private fun getTfLiteOptions(isOpenGPU: Boolean = true): Interpreter.Options { |
|
||||||
return Interpreter.Options().apply { |
|
||||||
if (isOpenGPU) this.addDelegate(GpuDelegate()) |
|
||||||
this.setNumThreads(config.numThreads) |
|
||||||
this.setAllowBufferHandleOutput(true) |
|
||||||
this.setAllowFp16PrecisionForFp32(true) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun loadModelFile(context: Context): MappedByteBuffer { |
|
||||||
val fileDescriptor = context.assets.openFd("nsfw.tflite") |
|
||||||
val inputStream = FileInputStream(fileDescriptor.fileDescriptor) |
|
||||||
val fileChannel = inputStream.channel |
|
||||||
val startOffset = fileDescriptor.startOffset |
|
||||||
val declaredLength = fileDescriptor.declaredLength |
|
||||||
return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength) |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
private fun convertBitmapToByteBuffer(bitmap_: Bitmap) { |
|
||||||
SystemClock.uptimeMillis().let { startTime -> |
|
||||||
imgData.rewind() |
|
||||||
//把每个像素的颜色值转为int 存入intValues |
|
||||||
bitmap_.getPixels( |
|
||||||
intValues, |
|
||||||
0, |
|
||||||
INPUT_WIDTH, |
|
||||||
max((bitmap_.height - INPUT_HEIGHT) / 2, 0), |
|
||||||
max((bitmap_.width - INPUT_WIDTH) / 2, 0), |
|
||||||
INPUT_WIDTH, |
|
||||||
INPUT_HEIGHT |
|
||||||
) |
|
||||||
for (color in intValues) { |
|
||||||
imgData.putFloat((Color.blue(color) - 104).toFloat()) |
|
||||||
imgData.putFloat((Color.green(color) - 117).toFloat()) |
|
||||||
imgData.putFloat((Color.red(color) - 123).toFloat()) |
|
||||||
} |
|
||||||
"数据装载成功,耗时:${(SystemClock.uptimeMillis() - startTime)} ms".d() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// # 根据路径获取图片 Image.open(path) |
|
||||||
// # 判断图片的像素格式是否为RGB,如果不是RGB则转换为RGB(24位彩色图像,每个像素用24个bit表示,分别表示红色、绿色和蓝色三个通道) |
|
||||||
// # 重制图片大小为256*256并使用 官方解释:若要调整大小,请对所有可能影响输出值的像素使用线性插值计算输出像素值。对于其他变换,使用输入图像中2x2环境上的线性插值。 |
|
||||||
// # 对resize的结果转换为io流并保存为JPEG格式 |
|
||||||
// # Convert to 64-bit floating point.asType float32 定义变量存储转换后的32位float图片数据 |
|
||||||
// # 获取图片的宽高,截取 224*224大小 x从16位开始,取到16+224位置 y亦是如此 |
|
||||||
// # 将取的数值转换位float32 |
|
||||||
// # 将每一个颜色值*255 |
|
||||||
// # 将每一个颜色减去一定的阈值 104.... |
|
||||||
// # [[[127.64.-18]]] 转换为 [[[[127.64.-18]]]] |
|
||||||
// # 使用index关键字喂入模型 |
|
||||||
// # 删除所有单维度的条目 |
|
||||||
// # 输出扫描结果 |
|
||||||
fun run(bitmap: Bitmap): NsfwBean { |
|
||||||
SystemClock.uptimeMillis().let { startTime -> |
|
||||||
//缩放位图时是否应使用双线性过滤。如果这是正确的,则在缩放时将使用双线性滤波,从而以较差的性能为代价具有更好的图像质量。如果这是错误的,则使用最近邻居缩放,这将使图像质量较差但速度更快。推荐的默认设置是将滤镜设置为“ true”,因为双线性滤镜的成本通常很小,并且改善的图像质量非常重要 |
|
||||||
ByteArrayOutputStream().let { stream -> |
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) |
|
||||||
stream.close() |
|
||||||
convertBitmapToByteBuffer(Bitmap.createScaledBitmap(bitmap, 256, 256, true)) |
|
||||||
// out |
|
||||||
Array(1) { FloatArray(2) }.apply { |
|
||||||
tflite.run(imgData, this) |
|
||||||
val nsfw = NsfwBean(this[0][0], this[0][1]) |
|
||||||
"扫描完成[${nsfw}]耗时: ${(SystemClock.uptimeMillis() - startTime)} ms".d() |
|
||||||
return nsfw |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// fun bitmap2RGB(bitmap: Bitmap): ByteArray? { |
|
||||||
// val bytes = bitmap.byteCount //返回可用于储存此位图像素的最小字节数 |
|
||||||
// val buffer = |
|
||||||
// ByteBuffer.allocate(bytes) // 使用allocate()静态方法创建字节缓冲区 |
|
||||||
// bitmap.copyPixelsToBuffer(buffer) // 将位图的像素复制到指定的缓冲区 |
|
||||||
// val rgba = buffer.array() |
|
||||||
// val pixels = ByteArray(rgba.size / 4 * 3) |
|
||||||
// val count = rgba.size / 4 |
|
||||||
// |
|
||||||
// //Bitmap像素点的色彩通道排列顺序是RGBA |
|
||||||
// for (i in 0 until count) { |
|
||||||
// pixels[i * 3] = rgba[i * 4] //R |
|
||||||
// pixels[i * 3 + 1] = rgba[i * 4 + 1] //G |
|
||||||
// pixels[i * 3 + 2] = rgba[i * 4 + 2] //B |
|
||||||
// } |
|
||||||
// return pixels |
|
||||||
// } |
|
||||||
} |
|
@ -1,23 +0,0 @@ |
|||||||
package com.zwy.opennsfw.core |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
|
|
||||||
data class Config( |
|
||||||
/** |
|
||||||
* 是否开启GPU加速 |
|
||||||
*/ |
|
||||||
var isOpenGPU: Boolean = true, |
|
||||||
/** |
|
||||||
* 扫描占用的线程数 |
|
||||||
*/ |
|
||||||
var numThreads: Int = 1, |
|
||||||
/** |
|
||||||
* 全局配置的context |
|
||||||
*/ |
|
||||||
var context: Context?, |
|
||||||
|
|
||||||
/** |
|
||||||
* nsfw模型存放目录 |
|
||||||
*/ |
|
||||||
var nsfwModuleFilePath: String? = null |
|
||||||
) |
|
@ -1,46 +0,0 @@ |
|||||||
import android.graphics.Bitmap |
|
||||||
import android.graphics.BitmapFactory |
|
||||||
import android.util.Log |
|
||||||
import com.zwy.opennsfw.core.Classifier |
|
||||||
import java.io.File |
|
||||||
import java.math.RoundingMode |
|
||||||
import java.text.DecimalFormat |
|
||||||
|
|
||||||
private val TAG = "OpenNSFWLogTag" |
|
||||||
|
|
||||||
var mClassifier: Classifier? = null |
|
||||||
|
|
||||||
fun String.d() = Log.d(TAG, this) |
|
||||||
|
|
||||||
fun String.e() = Log.e(TAG, this) |
|
||||||
|
|
||||||
open class NsfwBean(val sfw: Float, val nsfw: Float) { |
|
||||||
override fun toString(): String { |
|
||||||
val floatFormat = DecimalFormat("0.0000") |
|
||||||
floatFormat.roundingMode = RoundingMode.HALF_UP |
|
||||||
// return "适宜度(非色情程度):${floatFormat.format(sfw * 100)}%,不适宜度(涉黄程度):${floatFormat.format(nsfw * 100)}%" |
|
||||||
return "sfw:$sfw,nsfw:$nsfw%" |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fun Bitmap.getNsfwScore(): NsfwBean { |
|
||||||
checkNull() |
|
||||||
return mClassifier!!.run(this) |
|
||||||
} |
|
||||||
|
|
||||||
fun File.getNsfwScore(): NsfwBean { |
|
||||||
checkNull() |
|
||||||
val bitmap = try { |
|
||||||
BitmapFactory.decodeFile(this.path) |
|
||||||
} catch (e: Exception) { |
|
||||||
val m = "File2Bitmap过程失败,请确认文件是否存在或是否为图片" |
|
||||||
m.e() |
|
||||||
throw RuntimeException(m) |
|
||||||
} |
|
||||||
return bitmap.getNsfwScore() |
|
||||||
} |
|
||||||
|
|
||||||
private fun checkNull() { |
|
||||||
if (mClassifier == null) throw RuntimeException("Classifier未初始化,请使用Classifier.Build().context(context)初始化后再试") |
|
||||||
} |
|
||||||
|
|
@ -1,16 +0,0 @@ |
|||||||
package com.zwy.opennsfw |
|
||||||
|
|
||||||
import org.junit.Assert.assertEquals |
|
||||||
import org.junit.Test |
|
||||||
|
|
||||||
/** |
|
||||||
* Example local unit test, which will execute on the development machine (host). |
|
||||||
* |
|
||||||
* See [testing documentation](http://d.android.com/tools/testing). |
|
||||||
*/ |
|
||||||
class ExampleUnitTest { |
|
||||||
@Test |
|
||||||
fun addition_isCorrect() { |
|
||||||
assertEquals(4, 2 + 2) |
|
||||||
} |
|
||||||
} |
|
After Width: | Height: | Size: 2.1 MiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 146 KiB |
After Width: | Height: | Size: 115 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 192 KiB |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 78 KiB |
@ -1,25 +1,65 @@ |
|||||||
package com.zww.sample |
package com.zww.sample |
||||||
|
|
||||||
|
import android.annotation.SuppressLint |
||||||
|
import android.graphics.Bitmap |
||||||
import android.graphics.BitmapFactory |
import android.graphics.BitmapFactory |
||||||
|
import android.graphics.Color |
||||||
import android.os.Bundle |
import android.os.Bundle |
||||||
|
import android.widget.TextView |
||||||
import androidx.appcompat.app.AppCompatActivity |
import androidx.appcompat.app.AppCompatActivity |
||||||
|
import androidx.recyclerview.widget.GridLayoutManager |
||||||
|
import com.bumptech.glide.Glide |
||||||
|
import com.chad.library.adapter.base.BaseQuickAdapter |
||||||
|
import com.chad.library.adapter.base.viewholder.BaseViewHolder |
||||||
|
import com.zwy.nsfw.NSFWScoreBean |
||||||
import com.zwy.nsfw.getNSFWScore |
import com.zwy.nsfw.getNSFWScore |
||||||
|
import kotlinx.android.synthetic.main.activity_main.* |
||||||
import java.util.* |
import java.util.* |
||||||
|
|
||||||
class MainActivity : AppCompatActivity() { |
class MainActivity : AppCompatActivity() { |
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) { |
override fun onCreate(savedInstanceState: Bundle?) { |
||||||
super.onCreate(savedInstanceState) |
super.onCreate(savedInstanceState) |
||||||
setContentView(R.layout.activity_main) |
setContentView(R.layout.activity_main) |
||||||
|
mRecyclerView.layoutManager = GridLayoutManager(this, 2) |
||||||
|
mRecyclerView.adapter = |
||||||
|
object : BaseQuickAdapter<MyNSFWBean, BaseViewHolder>(R.layout.item_main) { |
||||||
|
@SuppressLint("SetTextI18n") |
||||||
|
override fun convert(holder: BaseViewHolder, item: MyNSFWBean) { |
||||||
|
Glide.with(this@MainActivity).load(item.bitmap).into(holder.getView(R.id.iv)) |
||||||
|
holder.getView<TextView>(R.id.tv).apply { |
||||||
|
item.nsfwScoreBean.let { |
||||||
|
this.text = |
||||||
|
"nsfw:${it.nsfwScore}\nsfw:${it.sfwScore}\n扫描耗时:${it.timeConsumingToScanData} ms" |
||||||
|
if (it.nsfwScore>0.7){ |
||||||
|
this.setBackgroundColor(Color.parseColor("#C1FF0000")) |
||||||
|
}else if (it.nsfwScore>0.5){ |
||||||
|
this.setBackgroundColor(Color.parseColor("#C1FF9800")) |
||||||
|
}else { |
||||||
|
this.setBackgroundColor(Color.parseColor("#803700B3")) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
}.also { adapter -> |
||||||
|
adapter.setAnimationWithDefault(BaseQuickAdapter.AnimationType.ScaleIn) |
||||||
|
adapter.isAnimationFirstOnly = false |
||||||
|
|
||||||
resources.assets.also { asset -> |
resources.assets.also { asset -> |
||||||
asset.list("img")?.forEach { |
asset.list("img")?.forEach { |
||||||
"img/$it".also { assetFilePath -> |
"img/$it".also { assetFilePath -> |
||||||
BitmapFactory.decodeStream(asset.open(assetFilePath)).also { |
BitmapFactory.decodeStream(asset.open(assetFilePath)).also { |
||||||
it.getNSFWScore() |
adapter.addData(MyNSFWBean(it.getNSFWScore(), it)) |
||||||
it.recycle() |
} |
||||||
|
} |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
} |
} |
||||||
} |
|
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
|
data class MyNSFWBean(val nsfwScoreBean: NSFWScoreBean, val bitmap: Bitmap) |
||||||
|
@ -0,0 +1,25 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||||
|
android:layout_width="200dp" |
||||||
|
android:layout_height="200dp"> |
||||||
|
|
||||||
|
<ImageView |
||||||
|
android:id="@+id/iv" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
android:layout_margin="5dp" |
||||||
|
android:scaleType="centerCrop"/> |
||||||
|
|
||||||
|
<TextView |
||||||
|
android:id="@+id/tv" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:layout_alignParentBottom="true" |
||||||
|
android:layout_marginLeft="5dp" |
||||||
|
android:layout_marginBottom="5dp" |
||||||
|
android:textColor="#ffffff" |
||||||
|
android:gravity="center" |
||||||
|
android:layout_marginRight="5dp" |
||||||
|
android:background="#C1FF9800" /> |
||||||
|
</RelativeLayout> |