{"id":17248792,"url":"https://github.com/nomisrev/customloadview","last_synced_at":"2025-03-26T06:14:07.214Z","repository":{"id":103227651,"uuid":"56300257","full_name":"nomisRev/CustomLoadView","owner":"nomisRev","description":"A custom component","archived":false,"fork":false,"pushed_at":"2016-04-18T20:15:47.000Z","size":786,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-31T07:41:39.291Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nomisRev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-04-15T07:27:56.000Z","updated_at":"2023-06-13T13:14:30.000Z","dependencies_parsed_at":null,"dependency_job_id":"84ba2cf2-2657-473d-9365-0419b254ba31","html_url":"https://github.com/nomisRev/CustomLoadView","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomisRev%2FCustomLoadView","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomisRev%2FCustomLoadView/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomisRev%2FCustomLoadView/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomisRev%2FCustomLoadView/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nomisRev","download_url":"https://codeload.github.com/nomisRev/CustomLoadView/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245598315,"owners_count":20641884,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T06:42:08.501Z","updated_at":"2025-03-26T06:14:07.201Z","avatar_url":"https://github.com/nomisRev.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Custom Load View\n\u003cimg src=\"demo.gif\" width=\"240\" height=\"426\" /\u003e\n\n* Making a custom view can be quite troublesome if you've never done it before and depending on the use case.\n\n## onMeasure()\n\n* Before starting to draw, we should make sure that the `Canvas` we'll be drawing on has the size we want. Think of the `Canvas` as a piece of paper you draw on. If the paper is too small, your drawing won't fit. If your paper is to big, it will take an unnecessary amount of space in a collage or on the wall.\n* To follow next code snippet you should read and understand the **Layout** section, http://developer.android.com/reference/android/view/View.html#Layout\n  * If `WRAP_CONTENT` is set as size we want the view to be as small as possible with guarantee that the view will fit. `SIZE = MIN SIZE`\n  * When a size is specified as `EXACTLY` or as `MATCH_PARENT`, we want the view to have the exact size or the size of the parent. `SIZE = EXACT` or `SIZE = SIZE PARENT`\n  \n* When we take into account that for our use case we want the view to be square we get the following method.\n\n```\noverride fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n\n  val widthMode = MeasureSpec.getMode(widthMeasureSpec);\n  val widthSize = MeasureSpec.getSize(widthMeasureSpec);\n  val heightMode = MeasureSpec.getMode(heightMeasureSpec);\n  val heightSize = MeasureSpec.getSize(heightMeasureSpec);\n  var size: Int = 0\n    \n  when {\n    (layoutParams.width == MATCH_PARENT \u0026\u0026 layoutParams.height == MATCH_PARENT) -\u003e size = getExact(widthSize, heightSize)\n    (widthMode == MeasureSpec.EXACTLY \u0026\u0026 heightMode != MeasureSpec.EXACTLY) -\u003e size = getExact(widthSize, 0)\n    (heightMode == MeasureSpec.EXACTLY \u0026\u0026 widthMode != MeasureSpec.EXACTLY) -\u003e size = getExact(0, heightSize)\n    (heightMode == MeasureSpec.EXACTLY \u0026\u0026 widthMode == MeasureSpec.EXACTLY) -\u003e size = getExact(widthMode, heightMode)\n    else -\u003e size = minSize\n  }\n    \n  setMeasuredDimension(size, size);\n}\n```\n\n* The above piece of code will become clear in a moment, but **copying this useless!!** as this piece of code is the result of my specific use case and my decisions.\n* First thing we do is get all the `MeasureSpec` data we need, the measure modes and the sizes passed to us.\n* Understanding the modes, if a specific size is passed in xml, the mode for that size will be `MeasureSpec.EXACTLY`. Since in my use case I will be drawing a circle. I want a square canvas, thus I will take the exactly size as a side length. In case both are passed a specific size, we will set the biggest as the side length.\n* If both are set to match_parent, assign the maximum size\n* In all cases that don't match two conditions above, we will assign the minimum size.\n\n#### Padding support\n\n* Adding padding support for padding is not required, that's why it resides in a separate topic.\n* I recommend adding padding support from the start, to make your view more flexible and reusable and uniform with your code and the Android system. \n* Padding behavior:\n  * When we add padding to a view set to `WRAP_CONTENT` it's the equivalent of  the view being as small as possible plus the extra padding area around it. So adding padding to `SIZE = MIN SIZE + PADDING`\n  * When a size is specified `EXACTLY` or we set to the size to `MATCH_PARENT` the size of the view may **not** be increased, padding now reduces the \"useable\" space inside the canvas. So we can conclude that his has no effect in our `onMeasure()`method. `SIZE = EXACT SIZE` or `SIZE = SIZE PARENT`\n\n* Knowing this, it's quite easy to add padding functionality for us in our `onMeasure()` method. In the case that `minSize` would be determined as the size, we increase minSize with the padding.\n```\nvar w: Int = 0\nvar h: Int = 0\n\nwhen {\n  (layoutParams.width == MATCH_PARENT \u0026\u0026 layoutParams.height == MATCH_PARENT) -\u003e { w = getExact(widthSize, heightSize); h = w }\n  (widthMode == MeasureSpec.EXACTLY \u0026\u0026 heightMode != MeasureSpec.EXACTLY) -\u003e { w = getExact(widthSize, 0); h = w }\n  (heightMode == MeasureSpec.EXACTLY \u0026\u0026 widthMode != MeasureSpec.EXACTLY) -\u003e { w = getExact(0, heightSize); h= w }\n  (heightMode == MeasureSpec.EXACTLY \u0026\u0026 widthMode == MeasureSpec.EXACTLY) -\u003e { w = getExact(widthMode, heightMode); h= w }\n  else -\u003e { w = minSize + paddingStart + paddingEnd; h =  minSize + paddingTop + paddingBottom }\n}\n\nsetMeasuredDimension(w, h);\n```\n\n* This is all we need to do to add padding support in this use case. It might be quite different, but once your onMeasure works as intended. We can finally begin with the fun stuff!\n\n\n## onDraw()\n\n* This is where all the magic happens and you draw your very own view!\n* It is **important** that you keep your onDraw method as fast as possible. Since you have no control of how much it's called. Let's say it's slow, and another view calls `requestLayout()` your view's `onDraw()` might be called quite frequently. For that reason I choose to declare my variables outside of the `onDraw()` method.\n\n```\noverride fun onDraw(canvas: Canvas) {\n  paint.color = outerColor\n  canvas.drawArc(0f, 0f,width.toFloat(), height.toFloat(), 270f, (360f / max) * progress, true, paint)\n  \n  paint.color = innerColor\n  sizeInnerCircle = (width - sizeOuterCircle).toFloat()\n  canvas.drawCircle(width.toFloat() / 2, height.toFloat() / 2, sizeInnerCircle / 2, paint)\n}\n```\n\n* Let's break it down: We have an inner and an outer part.\n  * The inner part is our `FAB`, so let's just draw a circle with the origin in the center of the canvas and the radius half the size of the circle. The size of the circle is determined by the size of the canvas and the width of the \"progress\" arc around our `FAB`\n  * The outer part has a configurable width and is drawn below our inner part.\n  * If you are unsure on how the methods `drawArc()` and `drawCircle()` work, I recommend that your read the documentation (http://developer.android.com/reference/android/graphics/Canvas.html) or navigate to it in Android studio (cmd+click or ctrl+click)\n  \n\n#### Padding support\n\n* In the `onDraw()` is where the real padding support is implemented butt is more straight forward to implement than the `onMeasure()`. There is only one thing you need to taking into account when implementing support for padding in `onDraw()`, let's examine the following example.\n\n\u003cimg src=\"ondraw-padding-support.png\" width=\"443\" height=\"159\"/\u003e\n\n* Let's say the square underneath is our `Canvas` with a padding of x dp than the red border has a width of x dp. This padding or red border is the part we cannot draw within.\n* The code below shows the effect of adding padding support in `onDraw()`\n\n\n```\noverride fun onDraw(canvas: Canvas) {\n  paint.color = outerColor\n  topArc = (0 + paddingStart ).toFloat()\n  rightArc = (0 + paddingTop ).toFloat()\n  bottomArc = (width - paddingEnd).toFloat()\n  leftArc = (height - paddingBottom).toFloat()\n  canvas.drawArc(topArc, rightArc, bottomArc, leftArc, 270f, (360f / max) * progress, true, paint)\n\n  paint.color = innerColor\n  sizeInnerCircle = (width - paddingStart - paddingEnd - widthOuterArc).toFloat()\n  canvas.drawCircle(width.toFloat() / 2, height.toFloat() / 2, sizeInnerCircle / 2, paint)\n}\n```\n\n* So since our `drawArc` works with `rectF` system to determine the size of the arc. We can easily determine that the `rectF` is the green area in the above example.  So we can easily adjust the first piece of code to take the padding into account.\n* To draw the inner circle it is fairly easy too, we calculated the size based on the width of the arc and the width of the `Canvas`. The `Canvas` now becomes the `Canvas` minus the padding, so again the green area.\n\n## Adding custom attributes\n\n* This is something I prefer to do at the end. I work with properties in code like you can see above. When the custom view is finished I have a better idea of what properties I find essential to be configurable. I prefer this approach over going back and forth to add custom attributes.\n\n* First we'll define our xml attributes in a `attrs.xml` file which should be placed in the res directory.\n\n```\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e\n\u003cresources\u003e\n    \u003cdeclare-styleable name=\"LoadView\"\u003e\n        \u003cattr format=\"integer\" name=\"width_progress\"/\u003e\n        \u003cattr format=\"integer\" name=\"max\"/\u003e\n        \u003cattr format=\"color\" name=\"inner_color\"/\u003e\n        \u003cattr format=\"color\" name=\"outer_color\"/\u003e\n    \u003c/declare-styleable\u003e\n\u003c/resources\u003e\n```\n\n* Now all that's left is retrieving the attributes in code\n```\nprivate fun getAttrs(attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) {\n    val attributes = context.theme.obtainStyledAttributes(attrs, R.styleable.LoadView, defStyleAttr,\n                                                          defStyleRes);\n    \n  try {\n    innerColor = attributes.getColor(R.styleable.LoadView_inner_color,Color.GREEN)\n    outerColor  = attributes.getColor(R.styleable.LoadView_outer_color,Color.CYAN)\n    max = attributes.getInteger(R.styleable.LoadView_max,0)\n    widthOuterArc = attributes.getFloat(R.styleable.LoadView_width_progress,40f)\n  } finally {\n    attributes.recycle();\n  }\n}\n```\n\n* Since the attributes are retrieved and assigned to the correct properties in the constructor (or init in Kotlin) there is no need to `invalidate()` the view like we would do in a `setInnerColor()` because we are sure that `onDraw()` will be called after we retrieved the attributes.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnomisrev%2Fcustomloadview","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnomisrev%2Fcustomloadview","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnomisrev%2Fcustomloadview/lists"}